(function(global){
'use strict';
/**
 * @name phantasus
 * @namespace
 */
var phantasus = (typeof phantasus !== 'undefined') ? phantasus : {};
if (typeof module !== 'undefined' && module.exports) {
    module.exports = phantasus; // Node
} else if (typeof define === 'function' && define.amd) {
    define(function () { // AMD module
        return phantasus;
    });
} else {
  global.phantasus = phantasus; // browser global
}
phantasus.Util = function () {
};

phantasus.Util.RIGHT_ARROW = String.fromCharCode(8594);
/**
 * Add properties in c2 to c1
 *
 * @param {Object}
 *            c1 The object that will inherit from obj2
 * @param {Object}
 *            c2 The object that obj1 inherits from
 */
phantasus.Util.extend = function (c1, c2) {
  for (var key in c2.prototype) {
    if (!(key in c1.prototype)) {
      c1.prototype[key] = c2.prototype[key];
    }
  }
};
phantasus.Util.isFetchStreamingSupported = function () {
  return typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Chrome') !== -1;
};

phantasus.Util.viewPortSize = function () {
  return window.getComputedStyle(document.body, ':before').content.replace(
    /"/g, '');
};

phantasus.Util.TRACKING_ENABLED = true;
phantasus.Util.TRACKING_CODE_LOADED = false;
phantasus.Util.loadTrackingCode = function () {
  if (phantasus.Util.TRACKING_ENABLED && typeof window !== 'undefined' && typeof navigator !== 'undefined' && navigator.onLine) {
    if (phantasus.Util.TRACKING_CODE_LOADED) {
      return;
    } else if (typeof ga === 'undefined') {
      phantasus.Util.TRACKING_CODE_LOADED = true;
      (function (i, s, o, g, r, a, m) {
        i['GoogleAnalyticsObject'] = r;
        i[r] = i[r] || function () {
          (i[r].q = i[r].q || []).push(arguments);
        }, i[r].l = 1 * new Date();
        a = s.createElement(o),
          m = s.getElementsByTagName(o)[0];
        a.async = 1;
        a.src = g;
        m.parentNode.insertBefore(a, m);
      })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
    }
    if (typeof ga !== 'undefined') {
      ga('create', 'UA-53973555-1', 'auto', 'phantasus');
      ga('phantasus.send', 'pageview');
    }
    phantasus.Util.TRACKING_CODE_LOADED = true;
  }
};

phantasus.Util.measureScrollbar = function () {
  var $c = $(
    '<div style=\'position:absolute; top:-10000px; left:-10000px; width:100px; height:100px; overflow:scroll;\'></div>')
    .appendTo('body');
  var dim = {
    width: Math.max(0, $c.width() - $c[0].clientWidth),
    height: $c.height() - $c[0].clientHeight
  };
  $c.remove();
  return dim;
};
phantasus.Util.trackEvent = function (options) {
  if (typeof window !== 'undefined') {
    if (!phantasus.Util.TRACKING_CODE_LOADED) {
      phantasus.Util.loadTrackingCode();
    }
    if (phantasus.Util.TRACKING_CODE_LOADED && typeof ga !== 'undefined') {
      ga('phantasus.send', {
        hitType: 'event',
        eventCategory: options.eventCategory,
        eventAction: options.eventAction,
        eventLabel: options.eventLabel
      });
    }
  }

};

phantasus.Util.isString = function (value) {
  return typeof value === 'string' || value instanceof String;
};
/**
 *
 * @param val The value to determine the data type for.
 * @return {String} One of string, number, object, [string], [number], [object]
 */
phantasus.Util.getDataType = function (val) {
  var dataType;
  var isArray = phantasus.Util.isArray(val);
  if (isArray && val.length > 0) {
    val = val[0];
  }
  if (phantasus.Util.isString(val)) {
    dataType = 'string';
  } else if (_.isNumber(val)) {
    dataType = 'number';
  } else {
    dataType = 'object';
  }
  if (isArray) {
    dataType = '[' + dataType + ']';
  }
  return dataType;
};

/**
 * Checks whether supplied argument is an array
 */
phantasus.Util.isArray = function (array) {
  var types = [
    Array, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array,
    Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array,];
  // handle native arrays
  for (var i = 0, length = types.length; i < length; i++) {
    if (array instanceof types[i]) {
      return true;
    }
  }
  return false;
};
phantasus.Util.getWindowSearchObject = function () {
  var searchObject = {};
  var hashObject = {};
  if (window.location.search.length > 0) {
    searchObject = phantasus.Util.getQueryParams(window.location.search
      .substring(1));
  }
  if (window.location.hash.length > 0) {
    hashObject = phantasus.Util.getQueryParams(window.location.hash
      .substring(1));
  }
  return _.extend(hashObject, searchObject);
};

phantasus.Util.copyString = function (s) {
  return (' ' + s).substr(1);
  //return (' ' + s).slice(1);
  // var copy = [];
  // for (var i = 0, end = s.length; i < end; i++) {
  // 	copy.push(s[i]);
  // }
  // return copy.join('');
};
phantasus.Util.getQueryParams = function (s) {
  var params = {};
  if (!s) {
    return params;
  }
  var search = decodeURIComponent(s);
  var keyValuePairs = search.split('&');
  for (var i = 0; i < keyValuePairs.length; i++) {
    var pair = keyValuePairs[i].split('=');
    if (pair[1] != null && pair[1] !== '') {
      var array = params[pair[0]];
      if (array === undefined) {
        array = [];
        params[pair[0]] = array;
      }
      array.push(pair[1]);
    }
  }
  return params;
};
phantasus.Util.getScriptPath = function (name) {
  if (!name) {
    name = 'phantasus-latest.min.js';
  }
  var scripts = document.getElementsByTagName('script');
  for (var i = scripts.length - 1; i >= 0; i--) {
    var src = scripts[i].src;
    var index = src.lastIndexOf('/');
    if (index !== -1) {
      src = src.substring(index + 1);
    }
    if (src === name) {
      return scripts[i].src;
    }
  }

  // not found
  if (name === 'phantasus-latest.min.js') {
    return phantasus.Util.getScriptPath('phantasus.js');
  }
  // return 1st script
  return scripts.length > 0 ? scripts[0].src : '';
};

phantasus.Util.forceDelete = function (obj) {
  try {
    var _garbageCollector = (function () {
      var ef = URL.createObjectURL(new Blob([''], {
        type: 'text/javascript'
      })), w = new Worker(ef);

      URL.revokeObjectURL(ef);
      return w;
    })();

    _garbageCollector.postMessage(obj, [obj]);
  }
  catch (x) {
    console.log('Unable to delete');
  }
};
phantasus.Util.getFileName = function (fileOrUrl) {
  if (phantasus.Util.isFile(fileOrUrl)) {
    return fileOrUrl.name;
  }
  if (fileOrUrl.name !== undefined) {
    return fileOrUrl.name;
  }
  var name = '' + fileOrUrl;
  var question = name.indexOf('?');
  if (question !== -1) {
    var params = name.substring(question + 1);
    var keyValuePairs = decodeURIComponent(params).split('&');

    // check for parameters in name
    for (var i = 0; i < keyValuePairs.length; i++) {
      var pair = keyValuePairs[i].split('=');
      if (pair[0] === 'file' || pair[0] === 'name') {
        name = pair[1];
        break;
      }
    }
  } else {
    var slash = name.lastIndexOf('/');
    if (slash === name.length - 1) {
      name = name.substring(0, name.length - 1);
      slash = name.lastIndexOf('/');
    }
    if (slash !== -1) {
      name = name.substring(slash + 1); // get stuff after slash
    }
  }
  return name;
};
phantasus.Util.prefixWithZero = function (value) {
  return value < 10 ? '0' + value : value;
};
phantasus.Util.getExtension = function (name) {
  name = '' + name;
  var dotIndex = name.lastIndexOf('.');
  if (dotIndex > 0) {
    var suffix = name.substring(dotIndex + 1).toLowerCase();
    if (suffix === 'txt' || suffix === 'gz' || suffix === 'tsv') { // see if file is in
      // the form
      // name.gct.txt
      var newPath = name.substring(0, dotIndex);
      var secondDotIndex = newPath.lastIndexOf('.');
      if (secondDotIndex > 0) {// see if file has another suffix
        var secondSuffix = newPath.substring(secondDotIndex + 1,
          newPath.length).toLowerCase();
        if (secondSuffix === 'segtab' || secondSuffix === 'seg'
          || secondSuffix === 'maf' || secondSuffix === 'gct'
          || secondSuffix === 'txt' || secondSuffix === 'gmt') {
          return secondSuffix;
        }
      }
    }
    return suffix;
  }
  return '';
};
/**
 * Gets the base file name. For example, if name is 'test.txt' the method
 * returns the string 'test'. If the name is 'test.txt.gz', the method also
 * returns the string 'test'.
 *
 * @param name
 *            The file name.
 * @return The base file name.
 */
phantasus.Util.getBaseFileName = function (name) {
  var dotIndex = name.lastIndexOf('.');
  if (dotIndex > 0) {
    var suffix = name.substring(dotIndex + 1, name.length);
    if (suffix === 'gz' || suffix === 'zip' || suffix === 'bz2') {
      return phantasus.Util.getBaseFileName(name.substring(0, dotIndex));
    }
    return name.substring(0, dotIndex);
  }
  return name;
};
phantasus.Util.seq = function (length) {
  var array = [];
  for (var i = 0; i < length; i++) {
    array.push(i);
  }
  return array;
};

phantasus.Util.sequ32 = function (length) {
  var array = new Uint32Array(length);
  for (var i = 0; i < length; i++) {
    array[i] = i;
  }
  return array;
};

/**
 * Converts window hash or search to an object that maps keys to an array of
 * values. For example ?foo=bar returns {foo:[bar]}
 */
phantasus.Util.paramsToObject = function (hash) {
  var search = hash ? window.location.hash : window.location.search;
  if (search.length <= 1) {
    return {};
  }
  search = decodeURIComponent(search);
  var keyValuePairs = search.substring(1).split('&');
  var result = {};
  for (var i = 0, length = keyValuePairs.length; i < length; i++) {
    var pair = keyValuePairs[i].split('=');
    var values = result[pair[0]];
    if (values === undefined) {
      values = [];
      result[pair[0]] = values;
    }
    values.push(pair[1]);
  }
  return result;
};

phantasus.Util.isHeadless = function () {
  return typeof $.ui === 'undefined';
};

phantasus.Util.isFile = function (f) {
  return typeof File !== 'undefined' && f instanceof File;
};
phantasus.Util.endsWith = function (string, suffix) {
  return string.length >= suffix.length
    && string.substr(string.length - suffix.length) === suffix;
};
phantasus.Util.measureSvgText = function (text, classname) {
  if (!text || text.length === 0) {
    return {
      height: 0,
      width: 0
    };
  }
  var container = d3.select('body').append('svg');
  if (classname) {
    container.attr('class', classname);
  }
  container.append('text').attr({
    x: -1000,
    y: -1000
  }).text(text);
  var bbox = container.node().getBBox();
  container.remove();
  return {
    height: bbox.height,
    width: bbox.width
  };
};
phantasus.Util.IS_MAC = false;
if (typeof navigator !== 'undefined') {
  phantasus.Util.IS_MAC = navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) ? true
    : false;
}
phantasus.Util.COMMAND_KEY = phantasus.Util.IS_MAC ? '&#8984;' : 'Ctrl+';

phantasus.Util.hammer = function (el, recognizers) {
  if (typeof Hammer !== 'undefined') {
    var hammer = new Hammer(el, {
      recognizers: []
    });

    if (_.indexOf(recognizers, 'pan') !== -1) {
      hammer.add(new Hammer.Pan({
        threshold: 1,
        direction: Hammer.DIRECTION_ALL
      }));
    } else if (_.indexOf(recognizers, 'panh') !== -1) {
      hammer.add(new Hammer.Pan({
        threshold: 1,
        direction: Hammer.DIRECTION_HORIZONTAL
      }));
    } else if (_.indexOf(recognizers, 'panv') !== -1) {
      hammer.add(new Hammer.Pan({
        threshold: 1,
        direction: Hammer.DIRECTION_VERTICAL
      }));
    }
    if (_.indexOf(recognizers, 'tap') !== -1) {
      // var singleTap = new Hammer.Tap({
      // event : 'singletap',
      // interval : 50
      // });
      // var doubleTap = new Hammer.Tap({
      // event : 'doubletap',
      // taps : 2
      // });
      // doubleTap.recognizeWith(singleTap);
      // singleTap.requireFailure([ doubleTap ]);
      // hammer.add([ doubleTap, singleTap ]);
      hammer.add(new Hammer.Tap());
    }
    if (_.indexOf(recognizers, 'pinch') !== -1) {
      hammer.add(new Hammer.Pinch());
    }
    if (_.indexOf(recognizers, 'longpress') !== -1) {
      hammer.add(new Hammer.Press({
        event: 'longpress',
        time: 1000
      }));
    }
    if (_.indexOf(recognizers, 'press') !== -1) {
      hammer.add(new Hammer.Press());
    }
    if (_.indexOf(recognizers, 'swipe') !== -1) {
      hammer.add(new Hammer.Swipe());
    }
    return hammer;
  } else {
    return $();
  }

};

phantasus.Util.createTextDecoder = function () {
  if (typeof TextDecoder !== 'undefined') {
    var textDecoder = new TextDecoder();
    return function (buf, start, end) {
      return textDecoder.decode(buf.subarray(start, end));
    };
  } else {
    return function (buf, start, end) {
      // TODO convert in chunks
      var s = [];
      for (var i = start; i < end; i++) {
        s.push(String.fromCharCode(buf[i]));
      }
      return s.join('');
    };
  }
};
phantasus.Util.autocompleteArrayMatcher = function (token, cb, array, fields, max) {
  var filteredSet = new phantasus.Set();
  var regex = new RegExp(phantasus.Util.escapeRegex(token), 'i');
  var regexMatch = new RegExp('(' + phantasus.Util.escapeRegex(token) + ')', 'i');
  // iterate through the pool of strings and for any string that
  // contains the substring `q`, add it to the `matches` array
  if (fields) {
    var nfields = fields.length;
    for (var i = 0, n = array.length; i < n; i++) {
      var item = array[i];
      for (var j = 0; j < nfields; j++) {
        var field = fields[j];
        var value = item[field];
        if (regex.test(value)) {
          filteredSet.add(value);
          break;
        }
      }
      if (filteredSet.size() === max) {
        break;
      }
    }
  } else {
    for (var i = 0, n = array.length; i < n; i++) {
      var value = array[i];
      if (regex.test(value)) {
        filteredSet.add(value);
        if (filteredSet.size() === max) {
          break;
        }
      }

    }
  }
  var matches = [];

  filteredSet.forEach(function (value) {
    var quotedValue = value;
    if (quotedValue.indexOf(' ') !== -1) {
      quotedValue = '"' + quotedValue + '"';
    }
    matches.push({
      value: quotedValue,
      label: '<span>' + value.replace(regexMatch, '<b>$1</b>')
      + '</span>'
    });
  });

  cb(matches);
};

/**
 *
 * @param array. Array of format,data
 */
phantasus.Util.setClipboardData = function (clipboardData, delay) {
  var isRTL = document.documentElement.getAttribute('dir') == 'rtl';
  var fakeElem = document.createElement('div');
  fakeElem.contentEditable = true;

  // Prevent zooming on iOS
  fakeElem.style.fontSize = '12pt';
  // Reset box model

  fakeElem.style.border = '0';
  fakeElem.style.padding = '0';
  fakeElem.style.margin = '0';
  // Move element out of screen horizontally
  fakeElem.style.position = 'absolute';
  fakeElem.style[isRTL ? 'right' : 'left'] = '-999999px';
  // Move element to the same position vertically
  fakeElem.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px';
  fakeElem.setAttribute('readonly', '');
  //fakeElem.innerHTML = html;
  var f = function (e) {
    clipboardData.forEach(function (elem) {
      e.clipboardData.setData(elem.format, elem.data);
    });

    e.preventDefault();
    e.stopPropagation();
    e.stopImmediatePropagation();
    fakeElem.removeEventListener('copy', f);
  };
  fakeElem.addEventListener('copy', f);

  document.body.appendChild(fakeElem);
  // if (fakeElem.hasAttribute('contenteditable')) {
  fakeElem.focus();
  // }
  var selection = window.getSelection();
  var range = document.createRange();
  range.selectNodeContents(fakeElem);
  selection.removeAllRanges();
  selection.addRange(range);
  if (delay) {
    setTimeout(function () {
      if (!document.execCommand('copy')) {
        console.log('copy failed');
      }
      document.body.removeChild(fakeElem);
    }, 20);
  } else {
    if (!document.execCommand('copy')) {
      console.log('copy failed');
    }
    document.body.removeChild(fakeElem);
  }
};

/**
 * @param {Number}
 *            [options.delay=500] - Delay to short autosuggestions.
 * @param {jQuery}
 *            options.$el - Text box to apply autosuggest to.
 * @param {Function}
 *            options.filter - Callback to invoke to filter a suggested term.
 *            Invoked with array of tokens and response.
 * @param {Function}
 *            options.select - Callback to invoke when a suggested term is
 *            selected.
 * @param {Boolean}
 *            [options.multi=true] - Whether to allow more than one search term.
 * @param {Boolean}
 *            [options.suggestWhenEmpty=true] - Whether to autosuggest terms
 *            when text field is empty.
 *
 */
phantasus.Util.autosuggest = function (options) {
  options = $.extend({}, {
    multi: true,
    delay: 500,
    minLength: 0,
    suggestWhenEmpty: true,
  }, options);

  var searching = false;

  function _select(event, ui, isKey) {
    if (ui.item.skip) {
      return false;
    }
    if (options.multi) {
      var terms = phantasus.Util
        .getAutocompleteTokens(
          options.$el[0].value,
          {
            trim: false,
            selectionStart: options.$el[0].selectionStart
          });

      var field = (event.toElement && event.toElement.dataset) ? event.toElement.dataset.autocomplete : null;
      var value = field ? ui.item[field] : ui.item.value;
      var show = ui.item.show;

      // replace the current input
      if (terms.length === 0) {
        terms.push(value);
      } else if (ui.item.clear) {
        terms = [value];
      } else {
        terms[terms.selectionStartIndex === -1
        || terms.selectionStartIndex === undefined ? terms.length - 1
          : terms.selectionStartIndex] = value;
      }
      // add the selected item
      options.$el[0].value = terms.join(' ');
      if ((show && !isKey) || (isKey && event.which === 13)) { // did
        // we
        // select
        // just a
        // field name?
        searching = true;
        setTimeout(function () {
          options.$el.autocomplete('search',
            options.$el.val());
        }, 20);
        setTimeout(function () {
          searching = false;
        }, 100);

      }
      if (!isKey && options.select) {
        options.select();
      }
      return false;
    }
    if (!isKey && options.select) {
      options.select();
    }
    if (!isKey && event.which === 13) {
      event.stopImmediatePropagation();
    }
  }

  options.$el
  // don't navigate away from the field on tab when selecting an item
    .on(
      'keydown',
      function (event) {
        if ((event.keyCode === $.ui.keyCode.TAB)
          && $(this).data('ui-autocomplete').menu.active) {
          event.preventDefault();
        }
      })
    .autocomplete(
      {
        minLength: options.minLength,
        delay: options.delay,
        source: function (request, response) {
          if (request.term.history && options.history) {
            return options.history(response);
          }
          // delegate back to autocomplete, but extract the
          // autocomplete term
          var terms = phantasus.Util
            .getAutocompleteTokens(
              request.term,
              {
                trim: false,
                selectionStart: options.$el[0].selectionStart
              });

          if (terms.selectionStartIndex === undefined
            || terms.selectionStartIndex === -1) {
            terms.selectionStartIndex = terms.length - 1;
          }
          if (options.suggestWhenEmpty || terms.length > 0) {
            options.filter(terms, response);
          }
        },
        focus: function (event, ui) {
          var original = event.originalEvent;
          while (original.originalEvent != null) {
            original = original.originalEvent;
          }
          if (original && /^key/.test(original.type)) {
            return _select(original, ui, true);
          }
          return false;
        },
        select: function (event, ui) {
          return _select(event, ui, false);
        }
      });

  // use html for label instead of default text, class for categories vs. items
  var instance = options.$el.autocomplete('instance');
  if (instance != null) {
    instance._renderItem = function (ul, item) {
      if (item.value == null) { // category
        return $('<li class="' + (item.class ? (' ' + item.class) : '') + ' search-category">')
          .append($('<div>').html(item.label))
          .appendTo(ul);
      }
      return $('<li class="' + (item.class ? (' ' + item.class) : '') + ' search-item">')
        .append($('<div>').html(item.label))
        .appendTo(ul);
    };
    instance._normalize = function (items) {
      return items;
    };
    instance._resizeMenu = function () {
      var ul = this.menu.element;
      ul.outerWidth(instance.element.outerWidth());
    };
  }
  var menu = options.$el.autocomplete('widget');
  menu.menu('option', 'items', '> :not(.search-category)');
  if (menu) {
    menu.addClass('search-menu');
  }
  if (options.suggestWhenEmpty) {
    options.$el.on('focus', function () {
      options.$el.autocomplete('search', options.$el.val());
    });
  }

  options.$el.on('keyup', function (e) {
    if (e.which === 13 && !searching) {
      options.$el.autocomplete('close');
    } else if (e.which === 38 && options.history) { // up arrow
      options.$el.autocomplete('search', {history: true});
    } else if (options.suggestWhenEmpty && options.$el.val() === '') {
      options.$el.autocomplete('search', '');
    }
  });

};

phantasus.Util.getAutocompleteTokens = function (text, options) {
  options = $.extend({}, {
    trim: true
  }, options);
  if (options.trim) {
    text = $.trim(text);
  }
  if (text === '') {
    return [];
  }
  var inQuote = false;
  var inParen = false;
  var tokens = [];
  var currentToken = [];

  for (var i = 0, n = text.length; i < n; i++) {
    var c = text[i];
    if (c === '"') {
      inQuote = !inQuote;
      currentToken.push(c);
    } else if (c === '(' || c === ')') {
      inParen = c === '(';
      currentToken.push(c);
    } else {
      if ((c === ' ' || c === '\t') && !inQuote && !inParen) {
        tokens.push({
          s: currentToken.join(''),
          inSelectionStart: currentToken.inSelectionStart
        });
        currentToken = []; // start new token
      } else { // add to current token
        currentToken.push(c);
      }
    }
    if (i === options.selectionStart - 1) {
      currentToken.inSelectionStart = true;
    }

  }

  tokens.push({
    s: currentToken.join(''),
    inSelectionStart: currentToken.inSelectionStart
  });
  // add trailing token
  if (!options.trim && !inQuote && text[text.length - 1] === ' ') {
    tokens.push({
      s: ' ',
      inSelectionStart: false
    });
  }
  // remove empty tokens
  // keep spaces at end of input "field:value" for next autocomplete
  var filteredTokens = [];
  var selectionStartIndex = -1;
  for (var i = 0, ntokens = tokens.length; i < ntokens; i++) {
    var token = tokens[i];
    var s = token.s;
    if (options.trim || i < (ntokens - 1)) {
      s = $.trim(s);
    }
    if (s !== '') {
      if (token.inSelectionStart) {
        selectionStartIndex = filteredTokens.length;
      }
      filteredTokens.push(s);
    }
  }
  filteredTokens.selectionStartIndex = selectionStartIndex;
  return filteredTokens;
};

phantasus.Util.showDialog = function ($el, title, options) {
  var $dialog = $('<div></div>');
  $el.appendTo($dialog);
  $dialog.appendTo($(document.body));
  if (!options) {
    options = {};
  }
  $dialog.dialog({
    width: 670,
    height: 590,
    title: title,
    // resizeStop : function(event, ui) {
    // var w = parseInt($dialog.width());
    // var h = parseInt($dialog.height());
    // //var d = Math.min(w, h);
    // svg.attr("width", w - 50);
    // svg.attr("height", h - 50);
    // chart.update();
    // },
    close: function (event, ui) {
      $dialog.remove();
      if (options.close) {
        options.close();
      }
    }
  });
};
/**
 * @param sheet
 *            An xlsx sheet
 * @param delim
 *            If a delim is specified each row, will contain a string separated
 *            by delim. Otherwise each row will contain an array.
 */
phantasus.Util.sheetToArray = function (sheet, delim) {
  var r = XLSX.utils.decode_range(sheet['!ref']);
  var rows = [];
  var colors = [];
  var header = [];
  for (var C = r.s.c; C <= r.e.c; ++C) {
    var val = sheet[XLSX.utils.encode_cell({
      c: C,
      r: r.s.r
    })];
    var txt = String(XLSX.utils.format_cell(val));
    header.push(txt);
  }
  for (var R = r.s.r; R <= r.e.r; ++R) {
    var row = [];
    var isRowEmpty = true;
    for (var C = r.s.c; C <= r.e.c; ++C) {
      var val = sheet[XLSX.utils.encode_cell({
        c: C,
        r: R
      })];
      if (!val) {
        row.push('');
        continue;
      }
      isRowEmpty = false;
      var txt = String(XLSX.utils.format_cell(val));
      if (val.s != null && val.s.fgColor != null) {
        var color = '#' + val.s.fgColor.rgb;
        colors.push({
          header: header[row.length],
          color: color,
          value: txt
        });
      }
      row.push(txt);
    }
    if (!isRowEmpty) {
      rows.push(delim ? row.join(delim) : row);
    }
  }
  rows.colors = colors;
  return rows;
};
phantasus.Util.linesToObjects = function (lines) {
  var header = lines[0];
  var array = [];
  var nfields = header.length;
  for (var i = 1, length = lines.length; i < length; i++) {
    var line = lines[i];
    var obj = {};
    for (var f = 0; f < nfields; f++) {
      var value = line[f];
      var field = header[f];
      obj[field] = value;
    }
    array.push(obj);
  }
  return array;
};
/**
 *
 * @param options.data Binary data string
 * @param options.prompt Prompt for sheet name
 * @param callback {Function} Callback
 */
phantasus.Util.xlsxTo2dArray = function (options, callback) {
  var workbook = XLSX.read(options.data, {
    type: 'binary',
    cellFormula: false,
    cellHTML: false,
    cellStyles: true
  });
  var sheetNames = workbook.SheetNames;
  if (options.prompt && sheetNames.length > 1) {
    var formBuilder = new phantasus.FormBuilder();
    formBuilder.append({
      name: 'sheet',
      type: 'bootstrap-select',
      options: sheetNames,
      required: true,
      style: 'max-width:100px;'
    });
    phantasus.FormBuilder.showInModal({
      title: 'Choose Sheet',
      html: formBuilder.$form,
      focus: document.activeElement,
      onClose: function () {
        var worksheet = workbook.Sheets[formBuilder.getValue('sheet')];
        var lines = phantasus.Util.sheetToArray(worksheet);
        callback(null, lines);
      }
    });

  } else {
    var worksheet = workbook.Sheets[sheetNames[0]];
    var lines = phantasus.Util.sheetToArray(worksheet);
    callback(null, lines);
  }

};
/**
 *
 * @param options.data Binary data string
 * @param options.prompt Prompt for sheet name
 * @param callback {Function} Callback
 */
phantasus.Util.xlsxTo1dArray = function (options, callback) {
  var workbook = XLSX.read(options.data, {
    type: 'binary',
    cellFormula: false,
    cellHTML: false,
    cellStyles: true
  });
  var sheetNames = workbook.SheetNames;
  if (options.prompt && sheetNames.length > 1) {
    var formBuilder = new phantasus.FormBuilder();
    formBuilder.append({
      name: 'sheet',
      type: 'bootstrap-select',
      options: sheetNames,
      required: true,
      style: 'max-width:100px;'
    });

    phantasus.FormBuilder.showOkCancel({
      title: 'Choose Sheet',
      cancel: false,
      focus: document.activeElement,
      content: formBuilder.$form,
      okCallback: function () {
        var worksheet = workbook.Sheets[formBuilder.getValue('sheet')];
        callback(null, phantasus.Util.sheetToArray(worksheet, '\t'));
      }
    });

  } else {
    var worksheet = workbook.Sheets[sheetNames[0]];
    callback(null, phantasus.Util.sheetToArray(worksheet, '\t'));
  }

};

/**
 * Returns a promise that resolves to a string
 */
phantasus.Util.getText = function (fileOrUrl) {
  var deferred = $.Deferred();
  if (phantasus.Util.isString(fileOrUrl)) {
    fetch(fileOrUrl).then(function (response) {
      if (response.ok) {
        return response.text();
      } else {
        deferred.reject(response.status + ' ' + response.statusText);
      }
    }).then(function (text) {
      // var type = xhr.getResponseHeader('Content-Type');
      deferred.resolve(text);
    }).catch(function (err) {
      deferred.reject(err);
    });
  } else if (phantasus.Util.isFile(fileOrUrl)) {
    var reader = new FileReader();
    reader.onload = function (event) {
      deferred.resolve(event.target.result);
    };
    reader.readAsText(fileOrUrl);
  } else {
    // what is fileOrUrl?
    deferred.resolve(fileOrUrl);
  }
  return deferred.promise();
};
phantasus.Util.createOptions = function (values, none) {
  var html = [];
  if (none) {
    html.push('<option value="">(None)</option>');
  }
  _.each(values, function (val) {
    html.push('<option value="');
    html.push(val);
    html.push('">');
    html.push(val);
    html.push('</option>');
  });
  return html.join('');
};

/**
 * Computes the rank using the given index array. The index array can be
 * obtained from the phantasus.Util.indexSort method. Does not handle ties.
 *
 * @param index
 * @return The ranks.
 */
phantasus.Util.rankIndexArray = function (index) {
  var rank = [];
  var n = index.length;
  for (var j = 0; j < n; j++) {
    rank[index[j]] = j + 1;
  }
  return rank;
};

phantasus.Util.indexSort = function (array, ascending) {
  var pairs = [];
  for(var i = 0, length = array.length; i < length; i++) {
    pairs.push({
      value: array[i],
      index: i
    });
  }
  return phantasus.Util.indexSortPairs(pairs, ascending);
};
phantasus.Util.indexSortPairs = function (array, ascending) {
  if (ascending) {
    array.sort(function (a, b) {
      return (a.value < b.value ? -1 : (a.value === b.value ? (a.index < b.index ? -1 : 1) : 1));
    });
  } else {
    array.sort(function (a, b) {
      return (a.value < b.value ? 1 : (a.value === b.value ? (a.index < b.index ? 1 : -1) : -1));
    });
  }
  var indices = [];
  array.forEach(function (item) {
    indices.push(item.index);
  });
  return indices;
};
phantasus.Util.arrayEquals = function (array1, array2, comparator) {
  if (array1 == array2) {
    return true;
  }
  if (array1 == null || array2 == null) {
    return false;
  }
  if (!comparator) {
    comparator = function (a, b) {
      return a === b;
    };
  }
  var length = array1.length;
  if (array2.length !== length) {
    return false;
  }
  for (var i = 0; i < length; i++) {
    if (!comparator(array1[i], array2[i])) {
      return false;
    }
  }
  return true;
};
phantasus.Util._intFormat = typeof d3 !== 'undefined' ? d3.format(',i')
  : function (d) {
  return '' + Math.round(d);
};
phantasus.Util.intFormat = function (n) {
  return phantasus.Util._intFormat(n);
};
phantasus.Util._nf = typeof d3 !== 'undefined' ? d3.format('.5g') : function (d) {
  return '' + d;
};

phantasus.Util.getNumberFormatPatternFractionDigits = function (pattern) {
  return parseInt(pattern.substring(1, pattern.length)) || 0;
};

phantasus.Util.nf = function (n) {
  // var str = (n < 1 && n > -1 && n.toPrecision !== undefined) ? n
  // .toPrecision(4) : phantasus.Util._nf(n);
  // return phantasus.Util.removeTrailingZerosInFraction(str);
  return phantasus.Util._nf(n);
};
phantasus.Util.createNumberFormat = function (pattern) {
  var f = d3.format(pattern);
  f.toJSON = function () {
    return {pattern: pattern};
  };
  return f;
};

phantasus.Util.wrapNumber = function (value, object) {
  var n = new Number(value);
  n.toObject = function () {
    return object;
  };
  return n;
};
phantasus.Util.toString = function (value) {
  if (value == null) {
    return '';
  } else if (_.isNumber(value)) {
    return phantasus.Util.nf(value);
  } else if (phantasus.Util.isArray(value)) {
    return phantasus.Util.arrayToString(value, ', ');
  }
  return '' + value;
};

phantasus.Util.arrayToString = function (value, sep) {
  var s = [];
  for (var i = 0, length = value.length; i < length; i++) {
    var val_i = value[i];
    if (_.isNumber(val_i)) {
      s.push(phantasus.Util.nf(val_i));
    } else {
      s.push('' + val_i);
    }

  }
  return s.join(sep);

};
phantasus.Util.removeTrailingZerosInFraction = function (str) {
  var index = str.lastIndexOf('.');
  if (str.lastIndexOf('e') !== -1) {
    return str;
  }
  if (index !== -1) {
    var len = str.length;
    var zeros = len;
    for (var i = len - 1; i > index; i--, zeros--) {
      if (str[i] != '0') {
        break;
      }
    }
    if (zeros === (index + 1)) {
      return str.substring(0, index);
    }
    if (zeros < len) {
      return str.substring(0, index) + str.substring(index, zeros);
    }
  }
  return str;
};
phantasus.Util.s = function (n) {
  return n === 1 ? '' : 's';
};
phantasus.Util.create2dArray = function (rows, columns) {
  var array2d = [];
  for (var i = 0; i < rows; i++) {
    var array = [];
    for (var j = 0; j < columns; j++) {
      array[j] = NaN;
    }
    array2d.push(array);
  }
  return array2d;
};
phantasus.Util.escapeRegex = function (value) {
  return value.replace(/[*]/g, '.*')
    .replace(/[-[\]{}()+?,\\^$|#\s]/g, '\\$&');
};

phantasus.Util.createSearchPredicates = function (options) {
  options = $.extend({}, {
    validateFieldNames: true,
    caseSensitive: true
  }, options);
  var tokens = options.tokens;
  if (tokens == null) {
    return [];
  }
  var availableFields = options.fields;
  if (!options.caseSensitive && availableFields != null) {
    for (var i = 0; i < availableFields.length; i++) {
      availableFields[i] = availableFields[i].toLowerCase();
    }
  }
  var validateFieldNames = options.validateFieldNames;
  var fieldSearchEnabled = !validateFieldNames
    || (availableFields != null && availableFields.length > 0);

  var fieldRegExp = /\\:/g;
  var predicates = [];
  var defaultIsExactMatch = options.defaultMatchMode === 'exact';

  tokens
    .forEach(function (token) {
      var isNot = false;
      if (token[0] === '-') { // not predicate
        token = token.substring(1);
        isNot = true;
      }
      var field = null;
      var semi = token.indexOf(':');
      if (semi > 0) { // field search?
        if (!fieldSearchEnabled
          || token.charCodeAt(semi - 1) === 92) { // \:
          token = token.replace(fieldRegExp, ':');
        } else { // only a field search if field matches
          // one of available fields
          var possibleToken = $.trim(token.substring(semi + 1));
          // check for "field":"val" and "field:val"
          var possibleField = $.trim(token.substring(0, semi)); // split
          // on :
          if (possibleField.length > 0
            && possibleField[0] === '"'
            && possibleField[possibleField.length - 1] === '"') {
            possibleField = possibleField.substring(1,
              possibleField.length - 1);
          } else if (possibleField.length > 0
            && possibleField[0] === '"'
            && possibleToken[possibleToken.length - 1] === '"'
            && possibleToken[0] !== '"') {
            possibleField = possibleField.substring(1,
              possibleField.length);
            possibleToken = '"' + possibleToken;

          }

          if (!validateFieldNames
            || availableFields.indexOf(options.caseSensitive ? possibleField : possibleField.toLowerCase()) !== -1) {
            token = possibleToken;
            field = possibleField;
          }
        }
      }

      var predicate;
      var rangeIndex = -1;
      var rangeToken = null;
      var rangeIndicators = ['..', '>=', '>', '<=', '<', '='];
      for (var i = 0; i < rangeIndicators.length; i++) {
        rangeIndex = token.indexOf(rangeIndicators[i]);
        if (rangeIndex !== -1) {
          rangeToken = rangeIndicators[i];
          break;
        }
      }

      if (rangeIndex !== -1) { // range query
        if (rangeToken === '..') {
          var start = parseFloat(token.substring(0, rangeIndex));
          var end = parseFloat(token.substring(rangeIndex + 2));
          if (!isNaN(start) && !isNaN(end)) {
            predicate = new phantasus.Util.NumberRangePredicate(
              field, start, end);
          }
        } else if (rangeToken === '>') {
          var val = parseFloat(token.substring(rangeIndex + 1));
          if (!isNaN(val)) {
            predicate = new phantasus.Util.GreaterThanPredicate(
              field, val);
          }
        } else if (rangeToken === '>=') {
          var val = parseFloat(token.substring(rangeIndex + 2));
          if (!isNaN(val)) {
            predicate = new phantasus.Util.GreaterThanOrEqualPredicate(
              field, val);
          }
        } else if (rangeToken === '<') {
          var val = parseFloat(token.substring(rangeIndex + 1));
          if (!isNaN(val)) {
            predicate = new phantasus.Util.LessThanPredicate(
              field, val);
          }
        } else if (rangeToken === '<=') {
          var val = parseFloat(token.substring(rangeIndex + 2));
          if (!isNaN(val)) {
            predicate = new phantasus.Util.LessThanOrEqualPredicate(
              field, val);
          }
        } else if (rangeToken === '=') {
          var val = parseFloat(token.substring(rangeIndex + 1));
          predicate = new phantasus.Util.EqualsPredicate(
            field, val);
        } else {
          console.log('Unknown range token:' + rangeToken);
        }
      } else if (token[0] === '"' && token[token.length - 1] === '"') { // exact
        token = token.substring(1, token.length - 1);
        predicate = new phantasus.Util.ExactTermPredicate(field,
          token);
      } else if (token[0] === '(' && token[token.length - 1] === ')') { // exact terms
        token = token.substring(1, token.length - 1);
        var values = phantasus.Util.getAutocompleteTokens(token);

        if (values.length > 0) {
          predicate = new phantasus.Util.ExactTermsPredicate(field,
            values.map(function (val) {
              if (val[0] === '"' && val[val.length - 1] === '"') {
                val = val.substring(1, val.length - 1);
              }
              return val.toLowerCase();
            }));
        }
      } else if (token.indexOf('*') !== -1) { // contains
        predicate = new phantasus.Util.RegexPredicate(field, token);
      } else {
        predicate = defaultIsExactMatch ? new phantasus.Util.ExactTermPredicate(
          field, token)
          : new phantasus.Util.RegexPredicate(field, token);

      }
      if (predicate != null) {
        predicates.push(isNot ? new phantasus.Util.NotPredicate(
          predicate) : predicate);
      }

    });
  return predicates;
}
;

phantasus.Util.createRegExpStringToMatchText = function (text) {
  var tokens = phantasus.Util.getAutocompleteTokens(text);
  if (tokens.length === 0) {
    return null;
  }
  var regex = [];
  _.each(tokens, function (token) {
    if (token[0] === '"' && token[token.length - 1] === '"') {
      token = token.substring(1, token.length - 1);
      regex.push('^' + phantasus.Util.escapeRegex(token) + '$'); // exact
      // match
    } else {
      regex.push(phantasus.Util.escapeRegex(token));
    }
  });
  return '(' + regex.join('|') + ')';
};
phantasus.Util.createRegExpToMatchText = function (text) {
  var s = phantasus.Util.createRegExpStringToMatchText(text);
  return s == null ? null : new RegExp(s, 'i');
};
phantasus.Util.reorderArray = function (array, index) {
  var newArray = [];
  for (var i = 0; i < index.length; i++) {
    newArray.push(array[index[i]]);
  }
  return newArray;
};
phantasus.Util.getSearchString = function () {
  var s = window.location.search;
  return s.length > 1 ? s.substring(1) : '';
};
/**
 * Takes an array of strings and splits each string by \t
 *
 * @return An array of arrays
 */
phantasus.Util.splitLines = function (lines) {
  var tab = /\t/;
  var tokens = [];
  for (var i = 0, nlines = lines.length; i < nlines; i++) {
    var line = lines[i];
    if (line === '') {
      continue;
    }
    tokens.push(line.split(tab));
  }
  return tokens;
};

/**
 * @param file
 *            a File or url
 * @return A deferred object that resolves to an array of strings
 */
phantasus.Util.readLines = function (fileOrUrl, interactive) {
  var isFile = phantasus.Util.isFile(fileOrUrl);
  var isString = phantasus.Util.isString(fileOrUrl);
  var name = phantasus.Util.getFileName(fileOrUrl);
  var ext = phantasus.Util.getExtension(name);
  var deferred = $.Deferred();
  if (isString) { // URL
    if (ext === 'xlsx') {
      var fetchOptions = {};
      if (fileOrUrl.headers) {
        fetchOptions.headers = new Headers();
        for (var header in fileOrUrl.headers) {
          fetchOptions.headers.append(header, fileOrUrl.headers[header]);
        }
      }
      fetch(fileOrUrl, fetchOptions).then(function (response) {
        if (response.ok) {
          return response.arrayBuffer();
        } else {
          deferred.reject(response);
        }
      }).then(function (arrayBuffer) {
        if (arrayBuffer) {
          var data = new Uint8Array(arrayBuffer);
          var arr = [];
          for (var i = 0; i != data.length; ++i) {
            arr[i] = String.fromCharCode(data[i]);
          }
          var bstr = arr.join('');
          phantasus.Util.xlsxTo1dArray({
            data: bstr,
            prompt: interactive
          }, function (err, lines) {
            deferred.resolve(lines);
          });

        } else {
          deferred.reject();
        }
      });
    } else {
      fetch(fileOrUrl, fetchOptions).then(function (response) {
        if (response.ok) {
          return response.text();
        } else {
          deferred.reject();
        }
      }).then(function (text) {
        deferred.resolve(phantasus.Util.splitOnNewLine(text));
      }).catch(function (err) {
        deferred.reject(err);
      });
    }
  } else if (isFile) {
    var reader = new FileReader();
    reader.onerror = function () {
      console.log('Unable to read file');
      deferred.reject('Unable to read file');
    };
    reader.onload = function (event) {
      if (ext === 'xlsx' || ext === 'xls') {
        var data = new Uint8Array(event.target.result);
        var arr = [];
        for (var i = 0; i != data.length; ++i) {
          arr[i] = String.fromCharCode(data[i]);
        }
        var bstr = arr.join('');
        phantasus.Util
          .xlsxTo1dArray({
            data: bstr,
            prompt: interactive
          }, function (err, lines) {
            deferred.resolve(lines);
          });
      } else {
        deferred.resolve(phantasus.Util.splitOnNewLine(event.target.result));
      }

    };
    if (ext === 'xlsx' || ext === 'xls') {
      reader.readAsArrayBuffer(fileOrUrl);
    } else {
      reader.readAsText(fileOrUrl);
    }
  } else { // it's already lines?
    deferred.resolve(fileOrUrl);
  }
  return deferred;
};
phantasus.Util.createValueToIndices = function (array, field) {
  var map = new phantasus.Map();
  _.each(array, function (item) {
    var key = item[field];
    var values = map.get(key);
    if (values === undefined) {
      values = [];
      map.set(key, values);
    }
    values.push(item);
  });
  return map;
};

phantasus.Util.createPhantasusHeader = function () {
  var html = [];

  html.push('<div style="margin-bottom:10px;"><svg width="32px" height="32px"><g><rect x="0" y="0" width="32" height="14" style="fill:#ca0020;stroke:none"/><rect x="0" y="18" width="32" height="14" style="fill:#0571b0;stroke:none"/></g></svg> <div data-name="brand" style="display:inline-block; vertical-align: top;font-size:24px;font-family:sans-serif;">');
  html.push('<span>P</span>');
  html.push('<span>h</span>');
  html.push('<span>a</span>');
  html.push('<span>n</span>');
  html.push('<span>t</span>');
  html.push('<span>a</span>');
  html.push('<span>s</span>');
  html.push('<span>u</span>');
  html.push('<span>s</span>');
  html.push('</span>');
  html.push('</div>');
  var $div = $(html.join(''));
  var colorScale = d3.scale.linear().domain([0, 4, 7]).range(['#ca0020', '#999999', '#0571b0']).clamp(true);
  var brands = $div.find('span');
  var index = 0;
  var step = function () {
    brands[index].style.color = colorScale(index);
    index++;
    if (index < brands.length) {
      setTimeout(step, 200);
    }
  };
  setTimeout(step, 500);
  return $div;
};
phantasus.Util.createLoadingEl = function () {
  return $(
    '<div style="overflow:hidden;text-align:center;"><i class="fa fa-spinner fa-spin fa-3x"></i><span style="padding-left:4px;vertical-align:middle;font-weight:bold;">Loading...</span></div>');
};
/**
 * Splits a string by the new line character, trimming whitespace
 */
phantasus.Util.splitOnNewLine = function (text, commentChar) {
  var commentCharCode = commentChar !== undefined ? commentChar.charCodeAt(0)
    : undefined;

  var lines = text.split(/\n/);
  if (lines.length === 1) {
    var tmp = text.split(/\r/); // old school mac?
    if (tmp.length > 1) {
      lines = tmp;
    }
  }

  var rows = [];
  var rtrim = /\s+$/;
  for (var i = 0, nlines = lines.length; i < nlines; i++) {
    var line = lines[i].replace(rtrim, '');
    if (line !== '') {
      if (commentCharCode !== undefined) {
        if (line.charCodeAt(0) !== commentCharCode) {
          rows.push(line);
        }
      } else {
        rows.push(line);
      }
    }
  }
  return rows;
};

phantasus.Util.ContainsPredicate = function (field, text) {
  this.field = field;
  text = text.toLowerCase();
  this.text = text;
};
phantasus.Util.ContainsPredicate.prototype = {
  accept: function (value) {
    if (value == null) {
      return false;
    }
    value = ('' + value).toLowerCase();
    return value.indexOf(this.text) !== -1;
  },
  getField: function () {
    return this.field;
  },
  getText: function () {
    return this.text;
  },
  isNumber: function () {
    return false;
  },
  toString: function () {
    return 'ContainsPredicate ' + this.field + ':' + this.text;
  }
};
phantasus.Util.ExactTermsPredicate = function (field, values) {
  this.field = field;
  this.values = new phantasus.Set();
  for (var i = 0, nvalues = values.length; i < nvalues; i++) {
    this.values.add(values[i]);
  }
};
phantasus.Util.ExactTermsPredicate.prototype = {
  accept: function (value) {
    if (value == null) {
      return false;
    }
    value = ('' + value).toLowerCase();
    return this.values.has(value);
  },
  getField: function () {
    return this.field;
  },
  getValues: function () {
    return this.values;
  },
  isNumber: function () {
    return false;
  },
  toString: function () {
    return 'ExactTermsPredicate ' + this.field + ':' + this.text;
  }
};

phantasus.Util.ExactTermPredicate = function (field, term) {
  this.field = field;
  term = term.toLowerCase();
  this.text = term;
};
phantasus.Util.ExactTermPredicate.prototype = {
  accept: function (value) {
    if (value == null) {
      return false;
    }
    value = ('' + value).toLowerCase();
    return value === this.text;
  },
  getField: function () {
    return this.field;
  },
  getText: function () {
    return this.text;
  },
  isNumber: function () {
    return false;
  },
  toString: function () {
    return 'ExactTermPredicate ' + this.field + ':' + this.text;
  }
};
phantasus.Util.RegexPredicate = function (field, text) {
  this.field = field;
  this.text = text;
  this.regex = new RegExp(phantasus.Util.escapeRegex(text), 'i');
};
phantasus.Util.RegexPredicate.prototype = {
  accept: function (value) {
    return this.regex.test('' + value);
  },
  getField: function () {
    return this.field;
  },
  getText: function () {
    return this.text;
  },
  isNumber: function () {
    return false;
  },
  toString: function () {
    return 'RegexPredicate ' + this.field + ':' + this.regex;
  }
};
phantasus.Util.NumberRangePredicate = function (field, min, max) {
  this.field = field;
  this.min = min;
  this.max = max;
};
phantasus.Util.NumberRangePredicate.prototype = {
  accept: function (value) {
    return value >= this.min && value <= this.max;
  },
  getField: function () {
    return this.field;
  },
  isNumber: function () {
    return true;
  },
  toString: function () {
    return 'NumberRangePredicate ' + this.field + ':' + this.min + '...'
      + this.max;
  }
};

phantasus.Util.GreaterThanPredicate = function (field, val) {
  this.field = field;
  this.val = val;
};
phantasus.Util.GreaterThanPredicate.prototype = {
  accept: function (value) {
    return value > this.val;
  },
  getField: function () {
    return this.field;
  },
  isNumber: function () {
    return true;
  }
};

phantasus.Util.GreaterThanOrEqualPredicate = function (field, val) {
  this.field = field;
  this.val = val;
};
phantasus.Util.GreaterThanOrEqualPredicate.prototype = {
    accept: function (value) {
        return value >= this.val;
    },
    getField: function () {
        return this.field;
    },
    isNumber: function () {
        return true;
    }
};
phantasus.Util.LessThanPredicate = function (field, val) {
  this.field = field;
  this.val = val;
};
phantasus.Util.LessThanPredicate.prototype = {
  accept: function (value) {
    return value < this.val;
  },
  getField: function () {
    return this.field;
  },
  isNumber: function () {
    return true;
  }
};
phantasus.Util.LessThanOrEqualPredicate = function (field, val) {
  this.field = field;
  this.val = val;
};
phantasus.Util.LessThanOrEqualPredicate.prototype = {
  accept: function (value) {
    return value <= this.val;
  },
  getField: function () {
    return this.field;
  },
  isNumber: function () {
    return true;
  }
};
phantasus.Util.EqualsPredicate = function (field, val) {
  this.field = field;
  this.val = val;
};
phantasus.Util.EqualsPredicate.prototype = {
  accept: function (value) {
    return value === this.val;
  },
  getField: function () {
    return this.field;
  },
  isNumber: function () {
    return true;
  }
};
phantasus.Util.NotPredicate = function (p) {
  this.p = p;
};
phantasus.Util.NotPredicate.prototype = {
  accept: function (value) {
    return !this.p.accept(value);
  },
  getField: function () {
    return this.p.getField();
  },
  isNumber: function () {
    return this.p.isNumber();
  },
  toString: function () {
    return 'NotPredicate ' + this.p;
  }
};


phantasus.Util.getFieldNames = function (rexp) {
  var strValues = rexp.attrValue[0].stringValue;
  var res = [];
  strValues.forEach(function (v) {
    res.push(v.strval);
  });
  return res;
};
phantasus.Util.getRexpData = function (rexp, rclass) {
  //console.log(rexp, rclass);
  var names = phantasus.Util.getFieldNames(rexp);
  //console.log('fieldNames', names);

  var data = {};
  for (var i = 0; i < names.length; i++) {
    var rexpV = rexp.rexpValue[i];
    data[names[i]] = {};
    if (rexpV.rclass == rclass.LIST) {
      data[names[i]] = phantasus.Util.getRexpData(rexpV, rclass);
    }
    if (rexpV.attrName.length > 0 && rexpV.attrName[0] == 'dim') {
      data[names[i]].dim = rexpV.attrValue[0].intValue;
    }
    if (rexpV.rclass == rclass.INTEGER) {
      if (rexpV.attrName.length > 0 && rexpV.attrName[0] == 'levels') {
        data[names[i]].values = [];
        rexpV.attrValue[0].stringValue.forEach(function (v) {
          data[names[i]].values.push(v.strval);
        })
      }
      else {
        data[names[i]].values = rexpV.intValue;
      }
    }
    else if (rexpV.rclass == rclass.REAL) {
      data[names[i]].values = rexpV.realValue;
    }
    else if (rexpV.rclass == rclass.STRING) {
      data[names[i]].values = [];
      rexpV.stringValue.forEach(function (v) {
        data[names[i]].values.push(v.strval);
      });
    }
  }
  return data;
};

phantasus.Util.getFilePath = function (session, str) {
  var splitted = str.split("/");
  var fileName = splitted[splitted.length - 1];
  return session.getLoc() + "files/" + fileName;
};

phantasus.Util.getTrueIndices = function (dataset) {
  //console.log('TrueIndices', dataset, dataset.dataset, dataset.dataset === undefined);
  var rowIndices = dataset.rowIndices ? dataset.rowIndices : [];
  var rows = phantasus.Util.getConsNumbers(rowIndices.length);
  var columnIndices = dataset.columnIndices ? dataset.columnIndices : [];
  var columns = phantasus.Util.getConsNumbers(columnIndices.length);
  var iter = 0;
  var savedDataset = dataset;
  //console.log("rows processing");
  while (dataset.dataset) {
    var transposed = dataset instanceof phantasus.TransposedDatasetView;
    var currentIndices = transposed ? dataset.columnIndices : dataset.rowIndices;
    if (currentIndices == undefined) {
      dataset = dataset.dataset;
      continue;
    }
    rowIndices = currentIndices;
    //console.log(iter, "rows:", rows.length, rows);
    var newRows = Array.apply(null, Array(rows.length)).map(Number.prototype.valueOf, 0);
    for (var i = 0; i < rows.length; i++) {
      newRows[i] = currentIndices[rows[i]];
    }
    rows = newRows;
    dataset = dataset.dataset;
    iter++;
  }
  iter = 0;
  //console.log("columns processing");
  dataset = savedDataset;
  while (dataset.dataset) {
    transposed = dataset instanceof phantasus.TransposedDatasetView;
    currentIndices = transposed ? dataset.rowIndices : dataset.columnIndices;
    if (currentIndices == undefined) {
      dataset = dataset.dataset;
      continue;
    }
    columnIndices = dataset.columnIndices;

    var newCols = Array.apply(null, Array(columns.length)).map(Number.prototype.valueOf, 0)
    for (i = 0; i < columns.length; i++) {
      newCols[i] = currentIndices[columns[i]];
    }
    columns = newCols;

    dataset = dataset.dataset;
    iter++;
  }

  //console.log("res", rows, columns);
  var conseqRows = phantasus.Util.getConsNumbers(dataset.rows);
  var conseqCols = phantasus.Util.getConsNumbers(dataset.columns);
  //console.log(conseqCols);
  var ans = {};
  //console.log(phantasus.Util.equalArrays(rows, conseqRows));
  if (phantasus.Util.equalArrays(rows, conseqRows) || rows.length == 0 && phantasus.Util.equalArrays(conseqRows, rowIndices)) {
    ans.rows = [];
  }
  else {
    ans.rows = rows.length > 0 ? rows : rowIndices;
  }
  //console.log(phantasus.Util.equalArrays(columns, conseqCols));
  if (phantasus.Util.equalArrays(columns, conseqCols) || columns.length == 0 && phantasus.Util.equalArrays(conseqCols, columnIndices)) {
    ans.columns = [];
  }
  else {
    ans.columns = columns.length > 0 ? columns : columnIndices;
  }
  //console.log(ans);
  return ans;
};

phantasus.Util.getConsNumbers = function (n) {
  var ar = [];
  for (var i = 0; i < n; i++) {
    ar.push(i);
  }
  return ar;
};

phantasus.Util.equalArrays = function (a, b) {
  if (a.length != b.length || a == null || b == null) {
    return false;
  }
  for (var i = 0; i < a.length; i++) {
    if (a[i] != b[i]) {
      return false;
    }
  }
  return true;
};

phantasus.Util.getMessages = function(session) {
  var url = session.getLoc() + "messages";
  $.ajax({
    url : url,
    success : function(result) {
      console.log(result);
    }
  });
};

phantasus.Util.setLibrary = function (libraryName) {
  if (!window.libraryPrefix) window.libraryPrefix = '/';

  ocpu.seturl(window.libraryPrefix + 'ocpu/library/' + libraryName + '/R');
};

phantasus.BlobFromPath = function () {
};
phantasus.BlobFromPath.getFileBlob = function (url, cb) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", url);
  xhr.responseType = "blob";
  xhr.addEventListener('load', function () {
    cb(xhr.response);
  });
  xhr.send();
};
// If we add lastModifiedDate to blob it will stay blob, not File. But this operation is prohibited
// To make it File we need name and mimo
// phantasus.BlobFromPath.blobToFile = function (blob) {
//   blob.lastModifiedDate = new Date();
//   return blob;
// };

phantasus.BlobFromPath.getFileObject = function (filePathOrUrl, cb) {
  phantasus.BlobFromPath.getFileBlob(filePathOrUrl, function (blob) {
    //cb(phantasus.BlobFromPath.blobToFile(blob));
    cb(blob);
  });
};

// code taken from KineticJS
phantasus.Events = function () {
};
phantasus.Events.prototype = {
  /**
   * Pass in a string of events delimmited by a space to bind multiple events
   * at once such as 'mousedown mouseup mousemove'. Include a namespace to
   * bind an event by name such as 'click.foobar'.
   *
   * @param {String}
   *            evtStr e.g. 'click', 'mousedown touchstart', 'mousedown.foo
   *            touchstart.foo'
   * @param {Function}
   *            handler The handler function is passed an event object
   */
  on: function (evtStr, handler) {
    if (!handler) {
      throw Error('Handler not specified');
    }
    if (!this.eventListeners) {
      this.eventListeners = {};
    }
    var events = evtStr.split(' '), len = events.length, n, event, parts, baseEvent, name;
    /*
     * loop through types and attach event listeners to each one. eg. 'click
     * mouseover.namespace mouseout' will create three event bindings
     */
    for (n = 0; n < len; n++) {
      event = events[n];
      parts = event.split('.');
      baseEvent = parts[0];
      name = parts[1] || '';
      // create events array if it doesn't exist
      if (!this.eventListeners[baseEvent]) {
        this.eventListeners[baseEvent] = [];
      }
      this.eventListeners[baseEvent].push({
        name: name,
        handler: handler
      });
    }
    return this;
  },
  getListeners: function () {
    if (!this.eventListeners) {
      this.eventListeners = {};
    }
    return this.eventListeners;
  },
  setListeners: function (eventListeners) {
    this.eventListeners = eventListeners;
  },
  /**
   * Fire an event.
   *
   * @param eventType
   * @param evt
   */
  trigger: function (eventType, evt) {
    if (!this.eventListeners) {
      this.eventListeners = {};
    }
    if (!evt) {
      evt = {};
    }
    evt.type = eventType;
    if (!evt.source) {
      evt.source = this;
    }
    var events = this.eventListeners[eventType];
    if (events) {
      var len = events.length;
      for (var i = 0; i < len; i++) {
        events[i].handler.apply(this, [evt]);
      }
    }
    return this;
  },
  /**
   * Remove event bindings. Pass in a string of event types delimmited by a
   * space to remove multiple event bindings at once such as 'mousedown
   * mouseup mousemove'. include a namespace to remove an event binding by
   * name such as 'click.foobar'. If you only give a name like '.foobar', all
   * events in that namespace will be removed.
   *
   * @param {String}
   *            evtStr e.g. 'click', 'mousedown.foo touchstart', '.foobar'
   */
  off: function (evtStr, handler) {
    if (!this.eventListeners) {
      this.eventListeners = {};
    }
    var events = (evtStr || '').split(' '), len = events.length, n, t, event, parts, baseEvent, name;
    if (!evtStr) {
      // remove all events
      for (t in this.eventListeners) {
        this._off(t, null, handler);
      }
    }
    for (n = 0; n < len; n++) {
      event = events[n];
      parts = event.split('.');
      baseEvent = parts[0];
      name = parts[1];
      if (baseEvent) {
        if (this.eventListeners[baseEvent]) {
          this._off(baseEvent, name, handler);
        }
      } else {
        for (t in this.eventListeners) {
          this._off(t, name, handler);
        }
      }
    }
    return this;
  },
  _off: function (type, name, handler) {
    var evtListeners = this.eventListeners[type], i, evtName;
    for (i = 0; i < evtListeners.length; i++) {
      evtName = evtListeners[i].name;
      // check if an event name is not specified, or if one is specified,
      // it matches the current event name
      if ((!name || evtName === name)
        && (handler == null || handler == evtListeners[i].handler)) {
        evtListeners.splice(i, 1);
        if (evtListeners.length === 0) {
          delete this.eventListeners[type];
          break;
        }
        i--;
      }
    }
  }
};

phantasus.Identifier = function (array) {
  this.array = array;
};
phantasus.Identifier.prototype = {
  toString: function () {
    return this.array.join(',');
  },
  equals: function (otherId) {
    var other = otherId.getArray();
    var length = this.array.length;
    if (other.length !== length) {
      return false;
    }
    for (var i = 0; i < length; i++) {
      if (this.array[i] !== other[i]) {
        return false;
      }
    }
    return true;
  },
  getArray: function () {
    return this.array;
  }
};

phantasus.Map = function () {
  this.map = {}; // object string -> key, value
  // the key field is stored to get the original key object back
  this.n = 0;
};
phantasus.Map.prototype = {
  toJSON: function () {
    var json = {};
    this.forEach(function (value, key) {
      json[key] = value;
    });
    return json;
  },
  toString: function () {
    var s = [];
    this.forEach(function (value, key) {
      if (s.length > 0) {
        s.push(', ');
      }
      s.push(key);
      s.push('=');
      s.push(value);
    });
    return s.join('');
  },
  keys: function () {
    var keys = [];
    for (var key in this.map) {
      var pair = this.map[key];
      keys.push(pair.key);
    }
    return keys;
  },
  size: function () {
    return this.n;
  },
  equals: function (m) {
    if (m.size() !== this.size()) {
      return false;
    }
    var ret = true;
    try {
      this.forEach(function (value, key) {
        if (value !== m.get(key)) {
          ret = false;
          throw 'break'; // break out of loop
        }
      });
    }
    catch (e) {
      // catch break out of loop
    }
    return ret;
  },
  setAll: function (map) {
    var _this = this;
    map.forEach(function (value, key) {
      _this.set(key, value);
    });
  },
  set: function (key, value) {
    var skey = '\0' + key;
    var previous = this.map[skey];
    if (previous === undefined) { // only increment size when this is a
      // new key
      this.n++;
    }
    this.map[skey] = {
      key: key,
      value: value
    };
  },
  forEach: function (callback) {
    for (var key in this.map) {
      var pair = this.map[key];
      callback(pair.value, pair.key);
    }
  },
  entries: function () {
    var array = [];
    this.forEach(function (value, key) {
      array.push({
        value: value,
        key: key
      });
    });
    return array;
  },
  values: function () {
    var values = [];
    for (var key in this.map) {
      var pair = this.map[key];
      values.push(pair.value);
    }
    return values;
  },
  get: function (key) {
    var skey = '\0' + key;
    var pair = this.map[skey];
    return pair !== undefined ? pair.value : undefined;
  },
  clear: function () {
    this.map = {};
    this.n = 0;
  },
  remove: function (key) {
    var skey = '\0' + key;
    var pair = this.map[skey];
    if (pair !== undefined) {
      delete this.map[skey];
      this.n--;
      return pair.value;
    }
  },
  has: function (key) {
    var skey = '\0' + key;
    return this.map[skey] !== undefined;
  }
};

phantasus.Map.fromJSON = function (json) {
  var map = new phantasus.Map();
  for (var key in json) {
    map.set(key, json[key]);
  }
  return map;
};

phantasus.ParseDatasetFromProtoBin = function () {
};

phantasus.ParseDatasetFromProtoBin.parse = function (session, callback, options) {
  session.getObject(function(success) {
    var filePath = phantasus.Util.getFilePath(session, JSON.parse(success)[0]);

    var r = new FileReader();

    r.onload = function (e) {
      var contents = e.target.result;
      var ProtoBuf = dcodeIO.ProtoBuf;
      ProtoBuf.protoFromFile("./message.proto", function (error, success) {
        if (error) {
          throw new Error(error);
        }
        var builder = success,
          rexp = builder.build("rexp"),
          REXP = rexp.REXP,
          rclass = REXP.RClass;


        var res = REXP.decode(contents);

        var jsondata = phantasus.Util.getRexpData(res, rclass);

        var datasets = [];
        for (var k = 0; k < Object.keys(jsondata).length; k++) {
          var dataset = phantasus.ParseDatasetFromProtoBin.getDataset(session, Object.keys(jsondata)[k],
                                                                      jsondata[Object.keys(jsondata)[k]], options);
          dataset.setESVariable('es_' + (k + 1).toString());
          datasets.push(dataset);
        }
        callback(null, datasets);
      });
    };

    phantasus.BlobFromPath.getFileObject(filePath, function (f) {
      r.readAsArrayBuffer(f);
    });
  })
};

phantasus.ParseDatasetFromProtoBin.getDataset = function (session, seriesName, jsondata, options) {
  var flatData = jsondata.data.values;
  var nrowData = jsondata.data.dim[0];
  var ncolData = jsondata.data.dim[1];
  var flatPdata = jsondata.pdata.values;
  var annotation = jsondata.fdata.values;
  //var id = jsondata.rownames.values;
  var metaNames = jsondata.colMetaNames.values;
  var rowMetaNames = jsondata.rowMetaNames.values;

  // console.log(seriesName, jsondata);

  var matrix = [];
  for (var i = 0; i < nrowData; i++) {
    var curArray = new Float32Array(ncolData);
    for (var j = 0; j < ncolData; j++) {
      curArray[j] = flatData[i + j * nrowData];
    }
    matrix.push(curArray);
  }
  var dataset = new phantasus.Dataset({
    name: seriesName,
    rows: nrowData,
    columns: ncolData,
    array: matrix,
    dataType: 'Float32',
    esSession: session,
    isGEO: options.isGEO,
    preloaded: options.preloaded
  });

  // console.log(seriesName, dataset);

  if (metaNames) {
    for (i = 0; i < metaNames.length; i++) {
      var curVec = dataset.getColumnMetadata().add(metaNames[i]);
      for (j = 0; j < ncolData; j++) {
        curVec.setValue(j, flatPdata[j + i * ncolData]);
      }
    }
  }
  // console.log(seriesName, "meta?");


  //var rowIds = dataset.getRowMetadata().add('id');

  // console.log(rowMetaNames);

  for (i = 0; i < rowMetaNames.length; i++) {
    curVec = dataset.getRowMetadata().add(rowMetaNames[i]);
    for (j = 0; j < nrowData; j++) {
      curVec.setValue(j, annotation[j + i * nrowData]);
      //rowIds.setValue(j, id[j])
    }
  }
  phantasus.MetadataUtil.maybeConvertStrings(dataset.getRowMetadata(), 1);
  phantasus.MetadataUtil.maybeConvertStrings(dataset.getColumnMetadata(),
    1);

  return dataset;
};
phantasus.Set = function () {
  this._map = new phantasus.Map();
};
phantasus.Set.prototype = {
  toJSON: function () {
    var json = [];
    this.forEach(function (value) {
      json.push(value);
    });
    return json;
  },
  toString: function () {
    var s = [];
    this.forEach(function (key) {
      if (s.length > 0) {
        s.push(', ');
      }
      s.push(key);
    });
    return s.join('');
  },
  size: function () {
    return this._map.size();
  },
  equals: function (m) {
    return this._map.equals(m);
  },
  forEach: function (callback) {
    this._map.forEach(function (value, key) {
      callback(key);
    });
  },
  add: function (value) {
    this._map.set(value, true);
  },
  values: function () {
    var values = [];
    this._map.forEach(function (value, key) {
      values.push(key);
    });
    return values;
  },
  clear: function () {
    this._map.clear();
  },
  remove: function (key) {
    this._map.remove(key);
  },
  has: function (key) {
    return this._map.has(key);
  }
};

phantasus.Set.fromJSON = function (json) {
  var set = new phantasus.Set();
  for (var i = 0, length = json.length; i < length; i++) {
    set.add(json[i]);
  }
  return set;
};

phantasus.ArrayBufferReader = function (buffer) {
  this.buffer = buffer;
  this.bufferLength = buffer.length;
  this.index = 0;
  this.decoder = phantasus.Util.createTextDecoder();
};

phantasus.ArrayBufferReader.prototype = {
  readLine: function () {
    var index = this.index;
    var bufferLength = this.bufferLength;
    if (index >= bufferLength) {
      return null;
    }
    var buffer = this.buffer;
    var start = index;
    var end = start;
    // dos: \r\n, old mac:\r
    for (; index < bufferLength; index++) {
      var c = buffer[index];
      if (c === 10 || c === 13) { // \n or \r
        end = index;
        if ((index !== bufferLength - 1) && buffer[index + 1] === 10) { // skip
          // ahead
          index++;
        }
        index++;
        break;
      }
    }
    this.index = index;
    if (start === end && index === bufferLength) { // eof
      return this.decoder(this.buffer, start, bufferLength);
    }

    return this.decoder(this.buffer, start, end);

  }
};

phantasus.ArrayBufferReader.getArrayBuffer = function (fileOrUrl, callback) {
  var isString = typeof fileOrUrl === 'string' || fileOrUrl instanceof String;
  if (isString) { // URL
    var fetchOptions = {};
    if (fileOrUrl.headers) {
      fetchOptions.headers = new Headers();
      for (var header in fileOrUrl.headers) {
        fetchOptions.headers.append(header, fileOrUrl.headers[header]);
      }
    }
    fetch(fileOrUrl, fetchOptions).then(function (response) {
      if (response.ok) {
        return response.arrayBuffer();
      } else {
        callback(new Error(fileOrUrl + ' status: ' + response.status));
      }
    }).then(function (buf) {
      callback(null, buf);
    }).catch(function (error) {
      console.log('Fetch error', error);
      callback(error);
    });

  } else {
    var reader = new FileReader();
    reader.onload = function (event) {
      callback(null, event.target.result);
    };
    reader.onerror = function (event) {
      callback(event);
    };
    reader.readAsArrayBuffer(fileOrUrl);
  }
};

phantasus.Array2dReaderInteractive = function () {

};

phantasus.Array2dReaderInteractive.prototype = {
  read: function (fileOrUrl, callback) {
    var _this = this;
    var name = phantasus.Util.getBaseFileName(phantasus.Util.getFileName(fileOrUrl));
    var html = [];
    html.push('<div>');
    html.push('<label>Click the table cell containing the first data row and column.</label>');
    html.push('<div class="checkbox"> <label> <input name="transpose" type="checkbox">' +
      ' Tranpose </label>' +
      ' </div>');

    html.push('<div' +
      ' style="display:inline-block;width:10px;height:10px;background-color:#b3cde3;"></div><span> Data Matrix</span>');
    html.push('<br /><div' +
      ' style="display:inline-block;width:10px;height:10px;background-color:#fbb4ae;"></div><span> Column' +
      ' Annotations</span>');
    html.push('<br /><div' +
      ' style="display:inline-block;' +
      ' width:10px;height:10px;background-color:#ccebc5;"></div><span> Row' +
      ' Annotations</span>');

    html.push('<div class="slick-bordered-table" style="width:550px;height:300px;"></div>');
    html.push('</div>');
    var $el = $(html.join(''));

    phantasus.Util.readLines(fileOrUrl, true).done(function (lines) {
      // show in table
      var tab = /\t/;
      for (var i = 0, nrows = lines.length; i < nrows; i++) {
        lines[i] = lines[i].split(tab);
      }
      var grid;
      var columns = [];
      for (var j = 0, ncols = lines[0].length; j < ncols; j++) {
        columns.push({
          id: j,
          field: j
        });
      }

      var dataRowStart = 1;
      var dataColumnStart = 1;
      var _lines = lines;
      var grid = new Slick.Grid($el.find('.slick-bordered-table')[0], lines, columns, {
        enableCellNavigation: true,
        headerRowHeight: 0,
        showHeaderRow: false,
        multiColumnSort: false,
        multiSelect: false,
        topPanelHeight: 0,
        enableColumnReorder: false,
        enableTextSelectionOnCells: true,
        forceFitColumns: false,
        defaultFormatter: function (row, cell, value, columnDef, dataContext) {
          var color = 'white';
          if (cell >= dataColumnStart && row >= dataRowStart) {
            color = '#b3cde3'; // data
          } else if (row <= (dataRowStart - 1) && cell >= dataColumnStart) {
            color = '#fbb4ae'; // column
          } else if (cell < dataColumnStart && row >= dataRowStart) {
            color = '#ccebc5'; // row
          }
          var html = ['<div style="width:100%;height:100%;background-color:' + color + '">'];
          if (_.isNumber(value)) {
            html.push(phantasus.Util.nf(value));
          } else if (phantasus.Util.isArray(value)) {
            var s = [];
            for (var i = 0, length = value.length; i < length; i++) {
              if (i > 0) {
                s.push(', ');
              }
              var val = value[i];
              s.push(value[i]);
            }
            html.push(s.join(''));
          } else {
            html.push(value);
          }
          html.push('</div>');
          return html.join('');
        }
      });
      var transposedLines;
      var transposedColumns;
      $el.find('[name=transpose]').on('click', function (e) {
        if ($(this).prop('checked')) {
          if (transposedLines == null) {
            transposedLines = [];
            for (var j = 0, ncols = lines[0].length; j < ncols; j++) {
              var row = [];
              transposedLines.push(row);
              for (var i = 0, nrows = lines.length; i < nrows; i++) {
                row.push(lines[i][j]);
              }
            }

            transposedColumns = [];
            for (var j = 0, ncols = transposedLines[0].length; j < ncols; j++) {
              transposedColumns.push({
                id: j,
                field: j
              });
            }

          }
          lines = transposedLines;
          grid.setData(transposedLines);
          grid.setColumns(transposedColumns);
          grid.resizeCanvas();
          grid.invalidate();
        } else {
          grid.setData(_lines);
          grid.setColumns(columns);
          grid.resizeCanvas();
          grid.invalidate();
          lines = _lines;
        }
      });
      grid.onClick.subscribe(function (e, args) {
        dataRowStart = args.row;
        dataColumnStart = args.cell;
        grid.invalidate();
      });

      $el.find('.slick-header').remove();
      var footer = [];
      footer
        .push('<button name="ok" type="button" class="btn btn-default">OK</button>');
      footer
        .push('<button name="cancel" type="button" data-dismiss="modal" class="btn btn-default">Cancel</button>');
      var $footer = $(footer.join(''));

      phantasus.FormBuilder.showOkCancel({
        title: 'Open',
        content: $el,
        close: false,
        focus: document.activeElement,
        cancelCallback: function () {
          callback(null);
        },
        okCallback: function () {
          _this._read(name, lines, dataColumnStart, dataRowStart, callback);
        }
      });
      grid.resizeCanvas();

    }).fail(function (err) {
      callback(err);
    });

  },
  _read: function (datasetName, lines, dataColumnStart, dataRowStart, cb) {
    var columnCount = lines[0].length;
    var columns = columnCount - dataColumnStart;
    var rows = lines.length - dataRowStart;
    var dataset = new phantasus.Dataset({
      name: datasetName,
      rows: rows,
      columns: columns,
      dataType: 'Float32'
    });

    // column metadata names are in 1st
    // column
    if (dataColumnStart > 0) {
      for (var i = 0; i < dataRowStart; i++) {
        var name = lines[i][0];
        if (name == null || name === '' || name === 'na') {
          name = 'id';
        }
        var unique = 1;
        while (dataset.getColumnMetadata().getByName(name) != null) {
          name = name + '-' + unique;
          unique++;
        }
        var v = dataset.getColumnMetadata().add(name);
        var nonEmpty = false;
        for (var j = 0; j < columns; j++) {
          var s = lines[i][j + dataColumnStart];
          if (s != null && s !== '') {
            nonEmpty = true;
            v.setValue(j, s);
          }
        }
        if (!nonEmpty) {
          dataset.getColumnMetadata().remove(phantasus.MetadataUtil.indexOf(dataset.getColumnMetadata(), v.getName()));
        }

      }
    }
    if (dataRowStart > 0) {
      // row metadata names are in first row
      for (var j = 0; j < dataColumnStart; j++) {
        var name = lines[0][j];
        if (name == null || name === '') {
          name = 'id';
        }
        var unique = 1;
        while (dataset.getRowMetadata().get(name) != null) {
          name = name + '-' + unique;
          unique++;
        }
        dataset.getRowMetadata().add(name);

      }
    }

    for (var i = 0; i < rows; i++) {
      for (var j = 0, k = 0; k < dataset.getRowMetadata().getMetadataCount(); j++, k++) {
        var metaDataValue = lines[i + dataRowStart][j];
        dataset.getRowMetadata().get(j).setValue(i, metaDataValue);
      }
    }

    for (var i = 0; i < rows; i++) {
      for (var j = 0; j < columns; j++) {
        var s = lines[i + dataRowStart][j + dataColumnStart];
        dataset.setValue(i, j, parseFloat(s));
      }
    }

    phantasus.MetadataUtil.maybeConvertStrings(dataset.getRowMetadata(), 1);
    phantasus.MetadataUtil.maybeConvertStrings(dataset.getColumnMetadata(),
      1);
    cb(null, dataset);
  }
};

phantasus.BufferedReader = function (reader, callback, doneCallback) {
  var textDecoder = phantasus.Util.createTextDecoder();
  var skipLF = false;
  var text = '';
  reader.read().then(function processResult(result) {
    // result contains a value which is an array of Uint8Array
    text += (result.done ? '' : textDecoder(result.value, 0, result.value.length));
    var start = 0;
    // TODO no need to search previous chunk of text
    for (var i = 0, length = text.length; i < length; i++) {
      var c = text[i];
      if (skipLF && c === '\n') {
        start++;
        skipLF = false;
      } else if (c === '\n' || c === '\r') {
        skipLF = c === '\r'; // \r\n windows line ending
        var s = phantasus.Util.copyString(text.substring(start, i));
        callback(s);
        start = i + 1;
      } else {
        skipLF = false;
      }
    }
    text = start < text.length ? text.substring(start) : '';
    if (!result.done) {
      return reader.read().then(processResult);
    } else {
      if (text !== '' && text !== '\r') {
        callback(text);
      }
      doneCallback();
    }
  });
};

phantasus.BufferedReader.parse = function (url, options) {
  var delim = options.delimiter;
  var regex = new RegExp(delim);
  var handleTokens = options.handleTokens;
  var complete = options.complete;

  var fetchOptions = {};
  if (url.headers) {
    fetchOptions.headers = new Headers();
    for (var header in url.headers) {
      fetchOptions.headers.append(header, url.headers[header]);
    }
  }
  fetch(url, fetchOptions).then(function (response) {
    if (response.ok) {
      var reader = response.body.getReader();
      new phantasus.BufferedReader(reader, function (line) {
        handleTokens(line.split(regex));
      }, function () {
        complete();
      });
    } else {
      options.error('Network error');
    }
  }).catch(function (error) {
    options.error(error);
  });
};


/**
 * Class for reading cls files. <p/> <p/> The CLS files are simple files created
 * to load class information into GeneCluster. These files use spaces to
 * separate the fields.
 * </P>
 * <UL>
 * <LI>The first line of a CLS file contains numbers indicating the number of
 * samples and number of classes. The number of samples should correspond to the
 * number of samples in the associated RES or GCT data file.</LI>
 * <p/>
 * <UL>
 * <LI>Line format: (number of samples) (space) (number of classes) (space) 1</LI>
 * <LI>For example: 58 2 1</LI>
 * </UL>
 * <p/>
 * <LI>The second line in a CLS file contains names for the class numbers. The
 * line should begin with a pound sign (#) followed by a space.</LI>
 * <p/>
 * <UL>
 * <LI>Line format: # (space) (class 0 name) (space) (class 1 name)</LI>
 * <p/>
 * <LI>For example: # cured fatal/ref.</LI>
 * </UL>
 * <p/>
 * <LI>The third line contains numeric class labels for each of the samples.
 * The number of class labels should be the same as the number of samples
 * specified in the first line.</LI>
 * <UL>
 * <LI>Line format: (sample 1 class) (space) (sample 2 class) (space) ...
 * (sample N class)</LI>
 * <LI>For example: 0 0 0 ... 1
 * </UL>
 * <p/>
 * </UL>
 */
phantasus.ClsReader = function () {
};
phantasus.ClsReader.prototype = {
  /**
   * Parses the cls file.
   *
   * @param lines
   *            The lines to read.
   * @throw Error If there is a problem with the data
   */
  read: function (lines) {
    var regex = /[ ,]+/;
    // header= <num_data> <num_classes> 1
    var header = lines[0].split(regex);
    if (header.length < 3) {
      throw new Error('Header line needs three numbers');
    }
    var headerNumbers = [];
    try {
      for (var i = 0; i < 3; i++) {
        headerNumbers[i] = parseInt($.trim(header[i]));
      }
    }
    catch (e) {
      throw new Error('Header line element ' + i + ' is not a number');
    }
    if (headerNumbers[0] <= 0) {
      throw new Error(
        'Header line missing first number, number of data points');
    }
    if (headerNumbers[1] <= 0) {
      throw new Error(
        'Header line missing second number, number of classes');
    }
    var numClasses = headerNumbers[1];
    var numItems = headerNumbers[0];
    var classDefinitionLine = lines[1];
    classDefinitionLine = classDefinitionLine.substring(classDefinitionLine
        .indexOf('#') + 1);
    var classNames = $.trim(classDefinitionLine).split(regex);
    if (classNames.length < numClasses) {
      throw new Error('First line specifies ' + numClasses
        + ' classes, but found ' + classNames.length + '.');
    }
    var dataLine = lines[2];
    var assignments = dataLine.split(regex);
    // convert the assignments to names
    for (var i = 0; i < assignments.length; i++) {
      var assignment = $.trim(assignments[i]);
      var index = parseInt(assignment);
      var tmp = classNames[index];
      if (tmp !== undefined) {
        assignments[i] = tmp;
      }
    }
    return assignments;
  }
};

phantasus.ClsWriter = function () {

};
phantasus.ClsWriter.prototype = {
  write: function (vector) {
    var pw = [];
    var size = vector.size();
    pw.push(size);
    pw.push(' ');
    var set = phantasus.VectorUtil.getSet(vector);
    pw.push(set.size());
    pw.push(' ');
    pw.push('1\n');
    pw.push('#');
    var valueToIndex = new phantasus.Map();
    var index = 0;
    set.forEach(function (name) {
      pw.push(' ');
      pw.push(name);
      valueToIndex.set(name, index++);
    });
    pw.push('\n');
    for (var i = 0; i < size; i++) {
      if (i > 0) {
        pw.push(' ');
      }
      pw.push(valueToIndex.get(vector.getValue(i)));
    }
    pw.push('\n');
    return pw.join('');
  }
};

phantasus.GctReader = function () {

};

phantasus.GctReader.prototype = {
  getFormatName: function () {
    return 'gct';
  },
  read: function (fileOrUrl, callback) {
    var _this = this;
    if (phantasus.Util.isFile(fileOrUrl)) {
      this._readChunking(fileOrUrl, callback, false);
    } else {
      if (phantasus.Util.isFetchStreamingSupported()) {
        this._readChunking(fileOrUrl, callback, true);
      } else {
        this._readNoChunking(fileOrUrl, callback);
      }
      // XXX only do byte range requests from S3
      // if (fileOrUrl.indexOf('s3.amazonaws.com') !== -1) {
      // 	$.ajax({
      // 		url: fileOrUrl,
      // 		method: 'HEAD'
      // 	}).done(function (data, textStatus, jqXHR) {
      // 		if ('gzip' === jqXHR.getResponseHeader('Content-Encoding')) {
      // 			_this._readNoChunking(fileOrUrl, callback);
      // 		} else {
      // 			_this._readChunking(fileOrUrl, callback, false);
      // 		}
      // 	}).fail(function () {
      // 		_this._readNoChunking(fileOrUrl, callback);
      // 	});
      // } else {
      // 	_this._readNoChunking(fileOrUrl, callback);
      // }
    }
  },
  _readChunking: function (fileOrUrl, callback, useFetch) {
    var _this = this;
    // Papa.LocalChunkSize = 10485760 * 10; // 100 MB
    //Papa.RemoteChunkSize = 10485760 / 2; // 10485760 = 10MB
    var lineNumber = 0;
    var version;
    var numRowAnnotations = 1; // in addition to row id
    var numColumnAnnotations = 0; // in addition to column id
    var nrows = -1;
    var ncols = -1;
    var version = 2;
    var rowMetadataNames = [];
    var columnMetadataNames = [];
    var rowMetadata = [[]];
    var columnMetadata = [[]];
    var dataColumnStart;
    var matrix = [];
    var dataMatrixLineNumberStart;
    var columnIdFieldName = 'id';
    var rowIdFieldName = 'id';
    var columnNamesArray;

    var handleTokens = function (tokens) {
      if (lineNumber === 0) {
        var text = tokens[0].trim();
        if ('#1.2' === text) {
          version = 2;
        } else if ('#1.3' === text) {
          version = 3;
        } else {
          // console.log('Unknown version: assuming version 2');
        }
      } else if (lineNumber === 1) {
        var dimensions = tokens;
        if (version === 3) {
          if (dimensions.length >= 4) {
            nrows = parseInt(dimensions[0]);
            ncols = parseInt(dimensions[1]);
            numRowAnnotations = parseInt(dimensions[2]);
            numColumnAnnotations = parseInt(dimensions[3]);
          } else { // no dimensions specified
            numRowAnnotations = parseInt(dimensions[0]);
            numColumnAnnotations = parseInt(dimensions[1]);
          }
        } else {
          nrows = parseInt(dimensions[0]);
          ncols = parseInt(dimensions[1]);
          if (nrows <= 0 || ncols <= 0) {
            callback(
              'Number of rows and columns must be greater than 0.');
          }
        }
        dataColumnStart = numRowAnnotations + 1;
      } else if (lineNumber === 2) {
        columnNamesArray = tokens;
        for (var i = 0; i < columnNamesArray.length; i++) {
          columnNamesArray[i] = phantasus.Util.copyString(columnNamesArray[i]);
        }
        if (ncols === -1) {
          ncols = columnNamesArray.length - numRowAnnotations - 1;
        }
        if (version == 2) {
          var expectedColumns = ncols + 2;
          if (columnNamesArray.length !== expectedColumns) {
            // check for trailing tabs
            if (columnNamesArray.length > expectedColumns) {
              var skip = columnNamesArray.length - 1;
              for (var i = columnNamesArray.length - 1; i >= 0; i--, skip--) {
                if (columnNamesArray[i] !== '') {
                  break;
                }
              }
              if (skip !== columnNamesArray.length - 1) {
                columnNamesArray = columnNamesArray.slice(0, skip + 1);
              }
            }
            if (columnNamesArray.length !== expectedColumns) {
              return callback('Expected ' + (expectedColumns - 2)
                + ' column names, but read '
                + (columnNamesArray.length - 2) + ' column names.');
            }
          }
        }
        var name = columnNamesArray[0];
        var slashIndex = name.lastIndexOf('/');

        if (slashIndex != -1 && slashIndex < (name.length - 1)) {
          rowIdFieldName = name.substring(0, slashIndex);
          columnIdFieldName = name.substring(slashIndex + 1);
        }
        rowMetadataNames.push(rowIdFieldName);
        columnMetadataNames.push(columnIdFieldName);
        for (var j = 0; j < ncols; j++) {
          var index = j + numRowAnnotations + 1;
          var columnName = index < columnNamesArray.length ? columnNamesArray[index]
            : null;
          columnMetadata[0].push(phantasus.Util.copyString(columnName));
        }

        for (var j = 0; j < numRowAnnotations; j++) {
          var rowMetadataName = '' === columnNamesArray[1] ? 'description'
            : columnNamesArray[j + 1];
          rowMetadataNames.push(
            rowMetadataName);
          rowMetadata.push([]);
        }
        dataMatrixLineNumberStart = 3 + numColumnAnnotations;
      } else { // lines >=3
        if (lineNumber < dataMatrixLineNumberStart) {
          var metadataName = phantasus.Util.copyString(tokens[0]);
          var v = [];
          columnMetadata.push(v);
          columnMetadataNames.push(metadataName);
          for (var j = 0; j < ncols; j++) {
            v.push(phantasus.Util.copyString(tokens[j + dataColumnStart]));
          }
        } else { // data lines
          if (tokens[0] !== '') {
            var array = new Float32Array(ncols);
            matrix.push(array);
            // we iterate to numRowAnnotations + 1 to include id row
            // metadata field
            for (var rowAnnotationIndex = 0; rowAnnotationIndex <= numRowAnnotations; rowAnnotationIndex++) {
              var rowMetadataValue = tokens[rowAnnotationIndex];
              rowMetadata[rowAnnotationIndex].push(
                phantasus.Util.copyString(rowMetadataValue));

            }

            for (var columnIndex = 0; columnIndex < ncols; columnIndex++) {
              var token = tokens[columnIndex + dataColumnStart];
              array[columnIndex] = parseFloat(token);
            }
          }
        }
      }
      lineNumber++;

    };
    (useFetch ? phantasus.BufferedReader : Papa).parse(fileOrUrl, {
      delimiter: '\t',	// auto-detect
      newline: '',	// auto-detect
      header: false,
      dynamicTyping: false,
      preview: 0,
      encoding: '',
      worker: false,
      comments: false,
      handleTokens: handleTokens,
      step: function (result) {
        handleTokens(result.data[0]);
      },
      complete: function () {
        var dataset = new phantasus.Dataset({
          name: phantasus.Util.getBaseFileName(phantasus.Util
            .getFileName(fileOrUrl)),
          rows: matrix.length,
          columns: ncols,
          array: matrix,
          dataType: 'Float32'
        });
        for (var i = 0; i < rowMetadataNames.length; i++) {
          dataset.getRowMetadata().add(rowMetadataNames[i]).array = rowMetadata[i];
        }
        for (var i = 0; i < columnMetadataNames.length; i++) {
          dataset.getColumnMetadata().add(columnMetadataNames[i]).array = columnMetadata[i];
        }
        phantasus.MetadataUtil.maybeConvertStrings(dataset.getRowMetadata(), 1);
        phantasus.MetadataUtil.maybeConvertStrings(dataset.getColumnMetadata(),
          1);
        callback(null, dataset);
      },
      error: function (err) {
        callback(err);
      },
      download: !phantasus.Util.isFile(fileOrUrl),
      skipEmptyLines: false,
      chunk: undefined,
      fastMode: true,
      beforeFirstChunk: undefined,
      withCredentials: undefined
    });
  },
  _read: function (datasetName, reader) {
    var tab = /\t/;
    var versionLine = phantasus.Util.copyString(reader.readLine().trim());
    if (versionLine === '') {
      throw new Error('Missing version line');
    }
    var version = 2;
    if ('#1.2' === versionLine) {
      version = 2;
    } else if ('#1.3' === versionLine) {
      version = 3;
    } else {
      // console.log('Unknown version: assuming version 2');
    }
    var dimensionsLine = phantasus.Util.copyString(reader.readLine());
    if (dimensionsLine == null) {
      throw new Error('No dimensions specified');
    }
    // <numRows> <tab> <numCols>
    var dimensions = dimensionsLine.split(/[ \t]/);
    var numRowAnnotations = 1; // in addition to row id
    var numColumnAnnotations = 0; // in addition to column id
    var nrows = -1;
    var ncols = -1;
    if (version === 3) {
      if (dimensions.length >= 4) {
        nrows = parseInt(dimensions[0]);
        ncols = parseInt(dimensions[1]);
        numRowAnnotations = parseInt(dimensions[2]);
        numColumnAnnotations = parseInt(dimensions[3]);
      } else { // no dimensions specified
        numRowAnnotations = parseInt(dimensions[0]);
        numColumnAnnotations = parseInt(dimensions[1]);
      }
    } else {
      nrows = parseInt(dimensions[0]);
      ncols = parseInt(dimensions[1]);
      if (nrows <= 0 || ncols <= 0) {
        throw new Error(
          'Number of rows and columns must be greater than 0.');
      }
    }
    var columnNamesLine = phantasus.Util.copyString(reader.readLine());
    if (columnNamesLine == null) {
      throw new Error('No column annotations');
    }

    var columnNamesArray = columnNamesLine.split(tab);
    if (ncols === -1) {
      ncols = columnNamesArray.length - numRowAnnotations - 1;
    }
    if (version == 2) {
      var expectedColumns = ncols + 2;
      if (columnNamesArray.length !== expectedColumns) {
        throw new Error('Expected ' + (expectedColumns - 2)
          + ' column names, but read '
          + (columnNamesArray.length - 2) + ' column names.');
      }
    }
    var name = columnNamesArray[0];
    var slashIndex = name.lastIndexOf('/');
    var columnIdFieldName = 'id';
    var rowIdFieldName = 'id';
    if (slashIndex != -1 && slashIndex < (name.length - 1)) {
      rowIdFieldName = name.substring(0, slashIndex);
      columnIdFieldName = name.substring(slashIndex + 1);
    }
    if (nrows === -1) {
      var matrix = [];
      var rowMetadataNames = [rowIdFieldName];
      var columnMetadataNames = [columnIdFieldName];
      var rowMetadata = [[]];
      var columnMetadata = [[]];
      for (var j = 0; j < ncols; j++) {
        var index = j + numRowAnnotations + 1;
        var columnName = index < columnNamesArray.length ? columnNamesArray[index]
          : null;
        columnMetadata[0].push(phantasus.Util.copyString(columnName));
      }

      for (var j = 0; j < numRowAnnotations; j++) {
        var rowMetadataName = '' === columnNamesArray[1] ? 'description'
          : columnNamesArray[j + 1];
        rowMetadataNames.push(
          rowMetadataName);
        rowMetadata.push([]);
      }

      var dataColumnStart = numRowAnnotations + 1;
      var ntokens = ncols + numRowAnnotations + 1;
      var linen = 3;
      if (numColumnAnnotations > 0) {
        for (var columnAnnotationIndex = 0; columnAnnotationIndex < numColumnAnnotations; columnAnnotationIndex++) {
          var tokens = reader.readLine().split(tab);
          var metadataName = tokens[0];
          var v = [];
          columnMetadata.push(v);
          columnMetadataNames.push(metadataName);
          for (var j = 0; j < ncols; j++) {
            v.push(phantasus.Util.copyString(tokens[j + dataColumnStart]));
          }
        }
      }

      var nonEmptyDescriptionFound = false;
      var numRowAnnotationsPlusOne = numRowAnnotations + 1;
      var s;
      while ((s = reader.readLine()) !== null) {
        if (s !== '') {
          var array = new Float32Array(ncols);
          matrix.push(array);
          var tokens = s.split(tab);
          // we iterate to numRowAnnotations + 1 to include id row
          // metadata field
          for (var rowAnnotationIndex = 0; rowAnnotationIndex < numRowAnnotationsPlusOne; rowAnnotationIndex++) {
            var rowMetadataValue = tokens[rowAnnotationIndex];
            rowMetadata[rowAnnotationIndex].push(
              phantasus.Util.copyString(rowMetadataValue));

          }

          for (var columnIndex = 0; columnIndex < ncols; columnIndex++) {
            var token = tokens[columnIndex + dataColumnStart];
            array[columnIndex] = parseFloat(token);
          }
        }

      }
      var dataset = new phantasus.Dataset({
        name: datasetName,
        rows: matrix.length,
        columns: ncols,
        array: matrix,
        dataType: 'Float32'
      });
      for (var i = 0; i < rowMetadataNames.length; i++) {
        dataset.getRowMetadata().add(rowMetadataNames[i]).array = rowMetadata[i];
      }
      for (var i = 0; i < columnMetadataNames.length; i++) {
        dataset.getColumnMetadata().add(columnMetadataNames[i]).array = columnMetadata[i];
      }
      phantasus.MetadataUtil.maybeConvertStrings(dataset.getRowMetadata(), 1);
      phantasus.MetadataUtil.maybeConvertStrings(dataset.getColumnMetadata(),
        1);
      return dataset;

    } else {
      var dataset = new phantasus.Dataset({
        dataType: 'Float32',
        name: datasetName,
        rows: nrows,
        columns: ncols
      });

      var columnIds = dataset.getColumnMetadata().add(columnIdFieldName);
      if (version == 3) {
        for (var j = 0; j < ncols; j++) {
          var index = j + numRowAnnotations + 1;
          var columnName = index < columnNamesArray.length ? columnNamesArray[index]
            : null;
          columnIds.setValue(j, phantasus.Util.copyString(columnName));
        }

      } else {
        for (var j = 0; j < ncols; j++) {
          var columnName = columnNamesArray[j + numRowAnnotations + 1];
          columnIds.setValue(j, phantasus.Util.copyString(columnName));
        }
      }

      var rowAnnotationVectors = [
        dataset.getRowMetadata().add(
          rowIdFieldName)];
      if (version === 3) {
        for (var j = 0; j < numRowAnnotations; j++) {
          var rowMetadataName = '' === columnNamesArray[1] ? 'description'
            : columnNamesArray[j + 1];
          rowAnnotationVectors.push(dataset.getRowMetadata().add(
            rowMetadataName));
        }

      } else {
        rowAnnotationVectors.push(dataset.getRowMetadata().add(
          columnNamesArray[1]));
      }

      var dataColumnStart = numRowAnnotations + 1;
      var ntokens = ncols + numRowAnnotations + 1;
      var linen = 3;
      if (numColumnAnnotations > 0) {
        for (var columnAnnotationIndex = 0; columnAnnotationIndex < numColumnAnnotations; columnAnnotationIndex++) {
          var tokens = reader.readLine().split(tab);
          var metadataName = tokens[0];
          var v = dataset.getColumnMetadata().add(metadataName);
          for (var j = 0; j < ncols; j++) {
            v.setValue(j, phantasus.Util.copyString(tokens[j + dataColumnStart]));
          }
        }
      }

      var nonEmptyDescriptionFound = false;
      var numRowAnnotationsPlusOne = numRowAnnotations + 1;
      for (var rowIndex = 0, nrows = dataset.getRowCount(); rowIndex < nrows; rowIndex++) {
        var s = reader.readLine();
        if (s === null) {
          throw new Error('Missing data rows.');
        }
        var tokens = s.split(tab);
        if (version === 2) {
          rowAnnotationVectors[0].setValue(rowIndex, phantasus.Util.copyString(tokens[0]));
          var desc = tokens[1];
          if (!nonEmptyDescriptionFound) {
            nonEmptyDescriptionFound = desc !== '';
          }
          rowAnnotationVectors[1].setValue(rowIndex, phantasus.Util.copyString(desc));
        } else {
          // we iterate to numRowAnnotations + 1 to include id row
          // metadata field
          for (var rowAnnotationIndex = 0; rowAnnotationIndex < numRowAnnotationsPlusOne; rowAnnotationIndex++) {
            var rowMetadataValue = tokens[rowAnnotationIndex];
            rowAnnotationVectors[rowAnnotationIndex].setValue(rowIndex,
              phantasus.Util.copyString(rowMetadataValue));

          }
        }
        for (var columnIndex = 0; columnIndex < ncols; columnIndex++) {
          var token = tokens[columnIndex + dataColumnStart];
          // if (token[0] === '{') {
          // var value = JSON.parse(token);
          // dataset.setValue(rowIndex, columnIndex, phantasus.Util
          // .wrapNumber(value.__v, value));
          // } else {
          // dataset.setValue(rowIndex, columnIndex, parseFloat(token));
          // }
          dataset.setValue(rowIndex, columnIndex, parseFloat(token));
        }

      }

      if (version === 2 && !nonEmptyDescriptionFound) {
        dataset.getRowMetadata().remove(1);
      }
      if (rowIndex !== nrows) {
        throw new Error('Missing data rows');
      }

      phantasus.MetadataUtil.maybeConvertStrings(dataset.getRowMetadata(), 1);
      phantasus.MetadataUtil.maybeConvertStrings(dataset.getColumnMetadata(),
        1);
      return dataset;
    }
  },
  _readNoChunking: function (fileOrUrl, callback) {
    var _this = this;
    var name = phantasus.Util.getBaseFileName(phantasus.Util
      .getFileName(fileOrUrl));
    phantasus.ArrayBufferReader.getArrayBuffer(fileOrUrl, function (err, arrayBuffer) {
      if (err) {
        callback(err);
      } else {
        callback(null, _this._read(name,
          new phantasus.ArrayBufferReader(new Uint8Array(
            arrayBuffer))));
      }
    });
    // $.ajax({
    // 	url: fileOrUrl,
    // 	dataType: 'text'
    // }).done(function (text) {
    // 	callback(null, _this.read(name, new phantasus.StringReader(text)));
    // }).fail(function (err) {
    // 	callback(err);
    // });

  }
};

phantasus.GctWriter = function () {
  this.nf = phantasus.Util.createNumberFormat('.5g');
};

phantasus.GctWriter.idFirst = function (model) {
  var fields = ['id', 'Id', 'pr_id'];
  var idIndex = -1;
  for (var i = 0; i < fields.length; i++) {
    idIndex = phantasus.MetadataUtil.indexOf(model, fields[i]);
    if (idIndex !== -1) {
      break;
    }
  }
  if (idIndex !== -1) {
    var order = [];
    order[0] = idIndex;
    for (var i = 0, j = 1, count = model.getMetadataCount(); i < count; i++) {
      if (i !== idIndex) {
        order[j++] = i;
      }
    }
    return new phantasus.MetadataModelColumnView(model, order);
  }
  return model;
};

phantasus.GctWriter.prototype = {
  setNumberFormat: function (nf) {
    this.nf = nf;
  },
  getExtension: function () {
    return 'gct';
  },
  write: function (dataset, pw) {
    if (pw == null) {
      pw = [];
    }
    var rowMetadata = phantasus.GctWriter.idFirst(dataset.getRowMetadata());
    var columnMetadata = phantasus.GctWriter.idFirst(dataset
      .getColumnMetadata());
    this.writeHeader(rowMetadata, columnMetadata, pw);
    this.writeData(dataset, rowMetadata, pw);

    //console.log(this, dataset, pw);

    return pw.join('');
  },
  writeData: function (dataset, rowMetadata, pw) {
    //console.log("writeData")
    var ncols = dataset.getColumnCount();
    var rowMetadataCount = rowMetadata.getMetadataCount();
    var nf = this.nf;
    for (var i = 0, rows = dataset.getRowCount(); i < rows; i++) {
      for (var rowMetadataIndex = 0; rowMetadataIndex < rowMetadataCount; rowMetadataIndex++) {
        if (rowMetadataIndex > 0) {
          pw.push('\t');
        }
        var vector = rowMetadata.get(rowMetadataIndex);
        var value = vector.getValue(i);

        if (value !== null) {
          var toString = phantasus.VectorTrack.vectorToString(vector);
          pw.push(toString(value));
        }
      }
      for (var j = 0; j < ncols; j++) {
        pw.push('\t');
        var value = dataset.getValue(i, j);
        pw.push(nf(value));
      }
      pw.push('\n');
    }
  },
  writeHeader: function (rowMetadata, columnMetadata, pw) {
    var rows = rowMetadata.getItemCount();
    var ncols = columnMetadata.getItemCount();
    pw.push('#1.3\n');
    var rowMetadataCount = rowMetadata.getMetadataCount();
    pw.push(rows + '\t' + ncols + '\t' + (rowMetadataCount - 1) + '\t'
      + (columnMetadata.getMetadataCount() - 1));
    pw.push('\n');
    for (var i = 0; i < rowMetadataCount; i++) {
      if (i > 0) {
        pw.push('\t');
      }
      var name = rowMetadata.get(i).getName();
      if (i === 0 && name !== columnMetadata.get(0).getName()) {
        name = name + '/' + columnMetadata.get(0).getName();
      }
      pw.push(name);
    }
    var toString = phantasus.VectorTrack.vectorToString(columnMetadata.get(0));
    for (var j = 0; j < ncols; j++) {
      pw.push('\t');
      pw.push(toString(columnMetadata.get(0).getValue(j)));
    }
    pw.push('\n');
    for (var columnMetadataIndex = 1, metadataSize = columnMetadata
      .getMetadataCount(); columnMetadataIndex < metadataSize; columnMetadataIndex++) {
      pw.push(columnMetadata.get(columnMetadataIndex).getName());
      for (var i = 1; i < rowMetadataCount; i++) {
        pw.push('\t');
        pw.push('na');
      }
      for (var j = 0; j < ncols; j++) {
        pw.push('\t');
        var vector = columnMetadata.get(columnMetadataIndex);
        var value = vector.getValue(j);
        if (value != null) {
          toString = phantasus.VectorTrack.vectorToString(columnMetadata.get(0));
          pw.push(toString(value));
        }
      }
      pw.push('\n');
    }
  }
};

phantasus.GctWriter12 = function () {
  this.options = {
    rowDescription: 'Description',
    rowId: 'id',
    columnId: 'id'
  };
  this.nf = phantasus.Util.createNumberFormat('.5g');
};
phantasus.GctWriter12.prototype = {
  setNumberFormat: function (nf) {
    this.nf = nf;
  },
  getExtension: function () {
    return 'gct';
  },
  write: function (dataset, pw) {
    if (pw == null) {
      pw = [];
    }
    var rows = dataset.getRowCount();
    var columns = dataset.getColumnCount();
    var version = '#1.2';
    pw.push(version);
    pw.push('\n');
    pw.push(rows + '\t' + columns);
    pw.push('\n');
    var rowMetadata = phantasus.GctWriter.idFirst(dataset.getRowMetadata());
    var columnMetadata = phantasus.GctWriter.idFirst(dataset
      .getColumnMetadata());
    pw.push('Name');
    pw.push('\t');
    pw.push('Description');
    var columnIds = columnMetadata.getByName(this.options.columnId);
    if (!columnIds) {
      columnIds = columnMetadata.get(0);
    }
    var columnIdToString = phantasus.VectorTrack.vectorToString(columnIds);
    for (var j = 0; j < columns; j++) {
      pw.push('\t');
      pw.push(columnIdToString(columnIds.getValue(j)));
    }
    var rowIds = rowMetadata.get(this.options.rowId);
    if (!rowIds) {
      rowIds = rowMetadata.get(0);
    }
    var rowDescriptions = rowMetadata
      .getByName(this.options.rowDescription);
    if (rowDescriptions == null && rowMetadata.getMetadataCount() > 1) {
      rowDescriptions = rowMetadata.get(1);
    }
    var rowIdToString = phantasus.VectorTrack.vectorToString(rowIds);
    var rowDescriptionToString = rowDescriptions != null ? phantasus.VectorTrack.vectorToString(rowDescriptions) : null;
    var nf = this.nf;
    for (var i = 0; i < rows; i++) {
      pw.push('\n');
      pw.push(rowIdToString(rowIds.getValue(i)));
      pw.push('\t');
      var rowDescription = rowDescriptions != null ? rowDescriptions
      .getValue(i) : null;
      if (rowDescription != null) {
        pw.push(rowDescriptionToString(rowDescription));
      }
      for (var j = 0; j < columns; j++) {
        pw.push('\t');
        pw.push(nf(dataset.getValue(i, j)));
      }
    }
    pw.push('\n');
    return pw.join('');
  }
};

phantasus.GeoReader = function () {
};

phantasus.GeoReader.prototype = {
  read: function (name, callback) {
    // console.log("read", name);
    var req = ocpu.call('loadGEO', { name: name }, function (session) {
      session.getMessages(function (success) {
        console.log('loadGEO messages', '::', success);
      });
      phantasus.ParseDatasetFromProtoBin.parse(session, callback, { isGEO : true });
    });
    req.fail(function () {
      callback(req.responseText);
    });

  },
  _parse: function (text) {

  }
};




phantasus.GisticReader = function () {

};
phantasus.GisticReader.prototype = {
  read: function (fileOrUrl, callback) {
    var _this = this;
    var name = phantasus.Util.getBaseFileName(phantasus.Util
      .getFileName(fileOrUrl));
    phantasus.ArrayBufferReader.getArrayBuffer(fileOrUrl, function (err,
                                                                   arrayBuffer) {
      if (err) {
        callback(err);
      } else {
        try {
          callback(null, _this._read(name,
            new phantasus.ArrayBufferReader(new Uint8Array(
              arrayBuffer))));
        }
        catch (x) {
          if (x.stack) {
            // console.log(x.stack);
          }
          callback(x);
        }
      }
    });

  },
  _read: function (datasetName, reader) {
    var tab = /\t/;
    var header = reader.readLine().trim().split(tab);

    // Unique Name, Descriptor, Wide Peak Limits, Peak Limits, Region
    // Limits, q values, Residual q values after removing segments shared
    // with higher peaks, Broad or Focal, Amplitude Threshold

    var ncols = header.length - 9;
    var matrix = [];
    var s;
    var rowIds = [];
    var qValues = [];
    while ((s = reader.readLine()) !== null) {
      s = s.trim();

      if (s !== '') {
        var tokens = s.split(tab);
        if (tokens[8] === 'Actual Copy Change Given') {
          var array = new Float32Array(ncols);
          matrix.push(array);
          rowIds.push(String($.trim(tokens[1])));
          qValues.push(parseFloat(tokens[5]));
          for (var j = 9; j <= ncols; j++) {
            var token = tokens[j];
            array[j - 9] = parseFloat(token);
          }
        }
      }
    }
    var dataset = new phantasus.Dataset({
      name: datasetName,
      rows: matrix.length,
      columns: ncols,
      array: matrix,
      dataType: 'Float32'
    });

    var columnIds = dataset.getColumnMetadata().add('id');
    for (var j = 0; j < ncols; j++) {
      columnIds.setValue(j, String(header[j + 9]));
    }

    dataset.getRowMetadata().add('id').array = rowIds;
    dataset.getRowMetadata().add('q_value').array = qValues;
    return dataset;
  }
};

phantasus.GmtDatasetReader = function () {
};
phantasus.GmtDatasetReader.prototype = {
  getFormatName: function () {
    return 'gmt';
  },
  read: function (fileOrUrl, callback) {
    var name = phantasus.Util.getBaseFileName(phantasus.Util
      .getFileName(fileOrUrl));
    phantasus.ArrayBufferReader.getArrayBuffer(fileOrUrl, function (err, arrayBuffer) {
      if (err) {
        callback(err);
      } else {
        try {
          callback(null, phantasus.DatasetUtil.geneSetsToDataset(name,
            new phantasus.GmtReader()
              .read(new phantasus.ArrayBufferReader(
                new Uint8Array(arrayBuffer)))));
        }
        catch (x) {
          callback(x);
        }
      }
    });

  }
};

phantasus.GmtReader = function () {
};
phantasus.GmtReader.prototype = {
  read: function (reader) {
    var sets = [];
    var tab = /\t/;
    var s;
    while ((s = reader.readLine()) != null) {
      if (s === '' || s[0] === '#') {
        continue;
      }
      var tokens = s.split(tab);
      var name = tokens[0].trim();
      var description = tokens.length > 1 ? tokens[1].trim() : '';
      if ('BLANK' === description) {
        description = '';
      }
      var ids = [];
      for (var i = 2; i < tokens.length; i++) {
        var geneName = tokens[i].trim();
        if (geneName !== '') {
          ids.push(geneName);
        }
      }
      var set = {
        name: name,
        description: description,
        ids: ids
      };
      set.toString = function () {
        return this.name;
      };
      sets.push(set);
    }
    return sets;
  }
};

phantasus.JsonDatasetReader = function () {

};

phantasus.JsonDatasetReader.prototype = {
  read: function (fileOrUrl, callback) {
    var _this = this;
    var name = phantasus.Util.getBaseFileName(phantasus.Util.getFileName(fileOrUrl));
    var isString = typeof fileOrUrl === 'string' || fileOrUrl instanceof String;
    if (isString) {
      fetch(fileOrUrl).then(function (response) {
        if (response.ok) {
          return response.text();
        } else {
          callback(response.status + ' ' + response.statusText);
        }
      }).then(function (text) {
        callback(null, phantasus.Dataset.fromJSON(JSON.parse(text.trim())));
      }).catch(function (err) {
        callback(err);
      });
    } else {
      var reader = new FileReader();
      reader.onload = function (event) {
        callback(null, phantasus.Dataset.fromJSON(JSON.parse(event.target.result)));
      };
      reader.onerror = function (event) {
        callback(event);
      };
      reader.readAsText(fileOrUrl);
    }

  }
};

phantasus.MafFileReader = function () {
  this.geneFilter = null;
};
/**
 *
 * @param options.dataset
 * @param options.fields
 */
phantasus.MafFileReader.summarizeMutations = function (options) {
  var dataset = options.dataset;
  var fields = options.fields;
  var count = fields.length;
  var vector = dataset.getRowMetadata().add('mutation_summary');
  vector.getProperties().set(
    phantasus.VectorKeys.FIELDS, fields);
  vector.getProperties().set(phantasus.VectorKeys.DATA_TYPE, '[number]');

  // computing dynamically screws things up b/c summary is computed for other data types (e.g. CN)
  for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
    var bins = new Int32Array(count); // 1-count
    for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
      var value = dataset.getValue(i, j);
      if (value > 0) {
        bins[value - 1]++;
      }
    }
    vector.setValue(i, bins);
  }
};

phantasus.MafFileReader.getField = function (fieldNames, headerToIndex) {
  var name;
  var index;

  for (var i = 0; i < fieldNames.length; i++) {
    name = fieldNames[i];

    var lc = name.toLowerCase();
    index = headerToIndex[lc];

    if (index !== undefined) {
      break;
    }
  }

  if (index !== undefined) {
    return {
      name: name,
      index: index
    };
  }
};

phantasus.MafFileReader.VARIANT_MAP = new phantasus.Map();
// silent
phantasus.MafFileReader.VARIANT_MAP.set('Silent', 1);
// in-frame indel
phantasus.MafFileReader.VARIANT_MAP.set('In_Frame_Del', 2);
phantasus.MafFileReader.VARIANT_MAP.set('In_Frame_Ins', 2);
phantasus.MafFileReader.VARIANT_MAP.set('Inframe_Del', 2);
phantasus.MafFileReader.VARIANT_MAP.set('Inframe_Ins', 2);

// other
phantasus.MafFileReader.VARIANT_MAP.set('Translation_Start_Site', 3);
phantasus.MafFileReader.VARIANT_MAP.set('Nonstop_Mutation', 3);
phantasus.MafFileReader.VARIANT_MAP.set('3\'UTR', 3);
phantasus.MafFileReader.VARIANT_MAP.set('3\'Flank', 3);
phantasus.MafFileReader.VARIANT_MAP.set('5\'UTR', 3);
phantasus.MafFileReader.VARIANT_MAP.set('5\'Flank', 3);
phantasus.MafFileReader.VARIANT_MAP.set('IGR', 3);
phantasus.MafFileReader.VARIANT_MAP.set('Intron', 3);
phantasus.MafFileReader.VARIANT_MAP.set('RNA', 3);
phantasus.MafFileReader.VARIANT_MAP.set('Targeted_Region', 3);
phantasus.MafFileReader.VARIANT_MAP.set('Unknown', 3);
phantasus.MafFileReader.VARIANT_MAP.set('1DEL', 3); // single copy loss from oncopanel
phantasus.MafFileReader.VARIANT_MAP.set('HA', 3); // high amplification from oncopanel

// mis-sense
phantasus.MafFileReader.VARIANT_MAP.set('Missense_Mutation', 4);
phantasus.MafFileReader.VARIANT_MAP.set('Missense', 4);

// splice site
phantasus.MafFileReader.VARIANT_MAP.set('Splice_Site', 5);
phantasus.MafFileReader.VARIANT_MAP.set('Splice_Acceptor', 5);
phantasus.MafFileReader.VARIANT_MAP.set('Splice_Region', 5);

// frame shift indel
phantasus.MafFileReader.VARIANT_MAP.set('Frame_Shift_Del', 6);
phantasus.MafFileReader.VARIANT_MAP.set('Frame_Shift_Ins', 6);
phantasus.MafFileReader.VARIANT_MAP.set('Frameshift', 6);

// non-sense
phantasus.MafFileReader.VARIANT_MAP.set('Nonsense_Mutation', 7);
phantasus.MafFileReader.VARIANT_MAP.set('Nonsense', 7);
phantasus.MafFileReader.VARIANT_MAP.set('2DEL', 7); // homozygous deletion from oncopanel

phantasus.MafFileReader.FIELD_NAMES = [
  'Synonymous', 'In Frame Indel', 'Other Non-Synonymous',
  'Missense', 'Splice Site', 'Frame Shift', 'Nonsense'];

phantasus.MafFileReader.prototype = {
  setGeneFilter: function (geneFilter) {
    this.geneFilter = geneFilter;
  },
  getFormatName: function () {
    return 'maf';
  },
  _getGeneLevelDataset: function (datasetName, reader) {
    var _this = this;
    var tab = /\t/;
    var header = reader.readLine().split(tab);
    var headerToIndex = {};
    for (var i = 0, length = header.length; i < length; i++) {
      headerToIndex[header[i].toLowerCase()] = i;
    }
    // TODO six classes of base substitution—C>A, C>G, C>T, T>A, T>C, T>G
    // (all substitutions are referred to by the pyrimidine of the mutated
    // Watson–Crick base pair)
    // var fields = ['Hugo_Symbol', 'Chromosome', 'Start_position',
    //   'Reference_Allele', 'Tumor_Seq_Allele2',
    //   'Variant_Classification', 'Protein_Change', 'Protein_Change', 'ccf_hat',
    //   'tumor_f', 'i_tumor_f', 'Tumor_Sample_Barcode', 'tumor_name',
    //   'Tumor_Sample_UUID', 'encoding'];
    //
    var sampleField = phantasus.MafFileReader.getField([
        'Tumor_Sample_Barcode', 'tumor_name', 'Tumor_Sample_UUID'],
      headerToIndex);
    var encodingField = phantasus.MafFileReader.getField([
        'encoding'],
      headerToIndex); // gives a numeric value for string
    if (sampleField == null) {
      throw new Error('Sample id column not found.');
    }
    var encodingColumnIndex = encodingField == null ? -1 : encodingField.index;
    var sampleColumnName = sampleField.name;
    var sampleIdColumnIndex = sampleField.index;
    var tumorFractionField = phantasus.MafFileReader.getField([
      'ccf_hat',
      'tumor_f', 'i_tumor_f'], headerToIndex);
    var ccfColumnName;
    var ccfColumnIndex;
    if (tumorFractionField !== undefined) {
      ccfColumnName = tumorFractionField.name;
      ccfColumnIndex = tumorFractionField.index;
    }
    var chromosomeColumn = headerToIndex['Chromosome'.toLowerCase()];
    var startPositionColumn = headerToIndex['Start_position'
      .toLowerCase()];
    var refAlleleColumn = headerToIndex['Reference_Allele'.toLowerCase()];
    var tumorAllelColumn = headerToIndex['Tumor_Seq_Allele2'
      .toLowerCase()];

    var proteinChangeColumn = headerToIndex['Protein_Change'.toLowerCase()];
    if (proteinChangeColumn == null) {
      proteinChangeColumn = headerToIndex['Protein'.toLowerCase()];
    }

    var geneSymbolColumn = headerToIndex['Hugo_Symbol'.toLowerCase()];
    if (geneSymbolColumn == null) {
      geneSymbolColumn = headerToIndex['gene'];
    }
    if (geneSymbolColumn == null) {
      throw new Error('Gene symbol column not found.');
    }
    var variantColumnIndex = headerToIndex['Variant_Classification'
      .toLowerCase()];
    if (variantColumnIndex == null) {
      variantColumnIndex = headerToIndex['variant'
        .toLowerCase()];
    }
    if (variantColumnIndex == null) {
      throw new Error('Variant_Classification not found');
    }
    // keep fields that are in file only

    var geneSymbolToIndex = new phantasus.Map();
    var sampleIdToIndex = new phantasus.Map();
    var variantMatrix = [];
    var ccfMatrix = [];
    var s;
    var customNumberToValueMap = new phantasus.Map();

    var hasMutationInfo = chromosomeColumn !== undefined && startPositionColumn !== undefined && refAlleleColumn !== undefined && tumorAllelColumn !== undefined;
    while ((s = reader.readLine()) !== null) {
      var tokens = s.split(tab);
      var sample = String(tokens[sampleIdColumnIndex]);
      var columnIndex = sampleIdToIndex.get(sample);
      if (columnIndex === undefined) {
        columnIndex = sampleIdToIndex.size();
        sampleIdToIndex.set(sample, columnIndex);
      }
      var gene = String(tokens[geneSymbolColumn]);
      if (gene === 'Unknown') {
        continue;
      }
      if (this.geneFilter == null
        || this.geneFilter.has(tokens[geneSymbolColumn])) {
        var rowIndex = geneSymbolToIndex.get(gene);
        if (rowIndex === undefined) {
          rowIndex = geneSymbolToIndex.size();
          geneSymbolToIndex.set(gene, rowIndex);
        }
        var value = String(tokens[variantColumnIndex]);
        var variantCode;
        if (encodingColumnIndex === -1) {
          variantCode = phantasus.MafFileReader.VARIANT_MAP.get(value);
          if (variantCode === undefined) {
            variantCode = 3;
          }
        } else {
          variantCode = parseInt(tokens[encodingColumnIndex]);
          customNumberToValueMap.set(variantCode, value);
        }

        var variantObject = {};
        var Protein_Change = tokens[proteinChangeColumn];
        if (Protein_Change) {
          variantObject.Protein = String(Protein_Change);
        }
        variantObject.__v = variantCode;
        variantObject.Variant = value;
        if (hasMutationInfo) {
          variantObject.Mutation = String(tokens[chromosomeColumn]) + ':'
            + String(tokens[startPositionColumn]) + ' '
            + String(tokens[refAlleleColumn]) + ' > '
            + String(tokens[tumorAllelColumn]);
        }
        var wrappedVariant = phantasus.Util.wrapNumber(variantCode,
          variantObject);
        var variantRow = variantMatrix[rowIndex];
        if (variantRow === undefined) {
          variantRow = [];
          variantMatrix[rowIndex] = variantRow;
        }
        var ccf = -1;
        var priorCcf = -1;
        if (ccfColumnIndex !== undefined) {
          var ccfRow = ccfMatrix[rowIndex];
          if (ccfRow === undefined) {
            ccfRow = [];
            ccfMatrix[rowIndex] = ccfRow;
          }
          ccf = parseFloat(tokens[ccfColumnIndex]);
          priorCcf = ccfRow[columnIndex] || -1;
        }
        var priorValue = variantRow[columnIndex] || -1;
        if (variantCode > priorValue) { // take most severe mutation
          variantRow[columnIndex] = wrappedVariant;
          if (ccfColumnIndex !== undefined) {
            ccfRow[columnIndex] = ccf;
          }
        } else if (variantCode === priorValue && ccf > priorCcf) {
          variantRow[columnIndex] = wrappedVariant;
          ccfRow[columnIndex] = ccf;
        }
      }
    }
    var dataset = new phantasus.Dataset({
      name: datasetName,
      array: variantMatrix,
      dataType: 'Number',
      rows: geneSymbolToIndex.size(),
      columns: sampleIdToIndex.size()
    });
    var columnIds = dataset.getColumnMetadata().add('id');
    sampleIdToIndex.forEach(function (index, id) {
      columnIds.setValue(index, id);
    });
    var rowIds = dataset.getRowMetadata().add('id');
    geneSymbolToIndex.forEach(function (index, id) {
      rowIds.setValue(index, id);
    });
    for (var i = 0, nrows = dataset.getRowCount(), ncols = dataset
      .getColumnCount(); i < nrows; i++) {
      for (var j = 0; j < ncols; j++) {
        if (variantMatrix[i][j] === undefined) {
          variantMatrix[i][j] = 0;
        }
      }
    }
    if (ccfColumnIndex !== undefined) {
      dataset.addSeries({
        dataType: 'Float32',
        name: 'allelic_fraction',
        array: ccfMatrix
      });
    }
    if (this.geneFilter) {
      var orderVector = dataset.getRowMetadata().add('order');
      for (var i = 0, size = orderVector.size(); i < size; i++) {
        var gene = rowIds.getValue(i);
        var order = this.geneFilter.get(gene);
        orderVector.setValue(i, order);
      }
      var project = new phantasus.Project(dataset);
      project.setRowSortKeys([
        new phantasus.SortKey('order',
          phantasus.SortKey.SortOrder.ASCENDING)], true); // sort
      // collapsed
      // dataset
      var tmp = project.getSortedFilteredDataset();
      project = new phantasus.Project(tmp);
      var columnIndices = phantasus.Util.seq(tmp.getColumnCount());
      columnIndices
        .sort(function (a, b) {
          for (var i = 0, nrows = tmp.getRowCount(); i < nrows; i++) {
            for (var seriesIndex = 0, nseries = tmp
              .getSeriesCount(); seriesIndex < nseries; seriesIndex++) {
              var f1 = tmp.getValue(i, a, seriesIndex);
              if (isNaN(f1)) {
                f1 = Number.NEGATIVE_INFINITY;
              }
              f1 = f1.valueOf();
              var f2 = tmp.getValue(i, b, seriesIndex);
              if (isNaN(f2)) {
                f2 = Number.NEGATIVE_INFINITY;
              }
              f2 = f2.valueOf();
              var returnVal = (f1 === f2 ? 0 : (f1 < f2 ? 1
                : -1));
              if (returnVal !== 0) {
                return returnVal;
              }
            }
          }
          return 0;
        });
      dataset = new phantasus.SlicedDatasetView(dataset, null,
        columnIndices);
    }

    var fieldNames = phantasus.MafFileReader.FIELD_NAMES;
    if (customNumberToValueMap.size() > 0) {
      var pairs = [];
      customNumberToValueMap.forEach(function (value, key) {
        pairs.push({
          key: key,
          value: value
        });
      });
      pairs.sort(function (a, b) {
        return (a.key === b.key ? 0 : (a.key < b.key ? -1 : 1));
      });
      fieldNames = pairs.map(function (p) {
        return p.value;
      });
    }
    var numUniqueValues = fieldNames.length;
    phantasus.MafFileReader.summarizeMutations({
      dataset: dataset,
      fields: fieldNames
    });
    phantasus.MafFileReader
      .summarizeMutations({
        dataset: new phantasus.TransposedDatasetView(dataset),
        fields: fieldNames
      });

    var mutationSummarySelectionVector = dataset.getColumnMetadata().add('mutation_summary_selection');
    mutationSummarySelectionVector.getProperties().set(
      phantasus.VectorKeys.FIELDS,
      fieldNames);
    mutationSummarySelectionVector.getProperties().set(phantasus.VectorKeys.DATA_TYPE, '[number]');
    mutationSummarySelectionVector.getProperties().set(phantasus.VectorKeys.RECOMPUTE_FUNCTION_SELECTION, true);
    var datasetName = dataset.getName();
    mutationSummarySelectionVector.getProperties().set(phantasus.VectorKeys.FUNCTION, {
      binSize: 1,
      domain: [1, 8],
      cumulative: false
    });
    // mutationSummarySelectionVector.getProperties().set(phantasus.VectorKeys.FUNCTION, function (view, selectedDataset, columnIndex) {
    //   var sourceVector = selectedDataset.getRowMetadata().getByName('Source');
    //   var bins = new Int32Array(numUniqueValues); // 1-7
    //   for (var i = 0, nrows = selectedDataset.getRowCount(); i < nrows; i++) {
    //     var source = sourceVector.getValue(i);
    //     if (source == null || source === datasetName) {
    //       var value = selectedDataset.getValue(i, columnIndex);
    //       if (value > 0) {
    //         bins[value - 1]++;
    //       }
    //     }
    //   }
    //   return bins;
    // });

    return dataset;
  },
  read: function (fileOrUrl, callback) {
    var _this = this;
    var name = phantasus.Util.getBaseFileName(phantasus.Util
      .getFileName(fileOrUrl));
    phantasus.ArrayBufferReader.getArrayBuffer(fileOrUrl, function (err, arrayBuffer) {
      if (err) {
        callback(err);
      } else {
        try {
          callback(null, _this._getGeneLevelDataset(name,
            new phantasus.ArrayBufferReader(new Uint8Array(
              arrayBuffer))));
        }
        catch (err) {
          callback(err);
        }
      }
    });

  }
};

phantasus.PreloadedReader = function () {
};

phantasus.PreloadedReader.prototype = {
  read: function(name, callback) {
    console.log("preloaded read", name);
    name = typeof name === "string" ? { name : name } : name;
    var req = ocpu.call('loadPreloaded', name, function(session) {
      phantasus.ParseDatasetFromProtoBin.parse(session, callback, { preloaded : true });
    });
    req.fail(function () {
      callback(req.responseText);
    })
  }
};
phantasus.SegTabReader = function () {
  this.regions = null;
};
phantasus.SegTabReader.binByRegion = function (dataset, regions) {

  var chromosomeVector = dataset.getRowMetadata().getByName('Chromosome');
  var startVector = dataset.getRowMetadata().getByName('Start_bp');
  var endVector = dataset.getRowMetadata().getByName('End_bp');

  var collapsedDataset = new phantasus.Dataset({
    name: dataset.getName(),
    rows: regions.length,
    columns: dataset.getColumnCount(),
    dataType: 'Float32'
  });
  phantasus.DatasetUtil.fill(collapsedDataset, NaN);
  var regionIdVector = collapsedDataset.getRowMetadata().add('id');
  var newChromosomeVector = collapsedDataset.getRowMetadata().add(
    'chromosome');
  var newStartVector = collapsedDataset.getRowMetadata().add('start');
  var newEndVector = collapsedDataset.getRowMetadata().add('end');
  var nsegmentsVector = collapsedDataset.getRowMetadata().add('nsegments');
  var nseries = dataset.getSeriesCount();

  for (var series = 1; series < nseries; series++) {
    collapsedDataset.addSeries({
      name: dataset.getName(series),
      dataType: 'Float32'
    });

  }

  var summarizeFunction = phantasus.Mean;
  collapsedDataset.setColumnMetadata(dataset.getColumnMetadata());
  for (var regionIndex = 0; regionIndex < regions.length; regionIndex++) {
    var region = regions[regionIndex];
    var rowIndices = [];
    for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
      var chromosome = chromosomeVector.getValue(i);
      var start = startVector.getValue(i);
      var end = endVector.getValue(i);
      if (region.chromosome == chromosome && start >= region.start
        && end <= region.end) {
        rowIndices.push(i);
      }
    }
    if (rowIndices.length > 0) {
      var slice = phantasus.DatasetUtil.slicedView(dataset, rowIndices,
        null);
      var columnView = new phantasus.DatasetColumnView(slice);
      for (var j = 0; j < dataset.getColumnCount(); j++) {
        columnView.setIndex(j);
        for (var series = 0; series < nseries; series++) {
          columnView.setSeriesIndex(series);
          collapsedDataset.setValue(regionIndex, j,
            summarizeFunction(columnView), series);
        }

      }
    }
    nsegmentsVector.setValue(regionIndex, rowIndices.length);
    regionIdVector.setValue(regionIndex, region.id);
    newChromosomeVector.setValue(regionIndex, region.chromosome);
    newStartVector.setValue(regionIndex, region.start);
    newEndVector.setValue(regionIndex, region.end);
  }
  return collapsedDataset;
};

phantasus.SegTabReader.prototype = {
  getFormatName: function () {
    return 'seg';
  },
  setRegions: function (regions) {
    this.regions = regions;
  },
  _read: function (datasetName, reader) {
    var tab = /\t/;
    var header = reader.readLine().split(tab);
    var fieldNameToIndex = {};
    for (var i = 0, length = header.length; i < length; i++) {
      var name = header[i].toLowerCase();
      fieldNameToIndex[name] = i;
    }

    var sampleField = phantasus.MafFileReader.getField(['pair_id',
      'Tumor_Sample_Barcode', 'tumor_name', 'Tumor_Sample_UUID',
      'Sample'], fieldNameToIndex);
    var sampleColumnName = sampleField.name;
    var sampleIdColumnIndex = sampleField.index;
    var tumorFractionField = phantasus.MafFileReader.getField(['ccf_hat',
      'tumor_f', 'i_tumor_f'], fieldNameToIndex);
    var ccfColumnName;
    var ccfColumnIndex;
    if (tumorFractionField !== undefined) {
      ccfColumnName = tumorFractionField.name;
      ccfColumnIndex = tumorFractionField.index;
    }
    var chromosomeColumn = fieldNameToIndex.Chromosome;
    var startPositionColumn = phantasus.MafFileReader.getField(['Start_bp',
      'Start'], fieldNameToIndex).index;
    var endPositionColumn = phantasus.MafFileReader.getField(['End_bp',
      'End'], fieldNameToIndex, {
      remove: false,
      lc: true
    }).index;
    var valueField = phantasus.MafFileReader.getField(['tau',
      'Segment_Mean']).index;
    var s;
    var matrix = [];
    var ccfMatrix = [];
    var sampleIdToIndex = new phantasus.Map();
    var chromosomeStartEndToIndex = new phantasus.Map();
    while ((s = reader.readLine()) !== null) {
      if (s === '') {
        continue;
      }
      var tokens = s.split(tab);
      var sample = String(tokens[sampleIdColumnIndex]);
      var columnIndex = sampleIdToIndex.get(sample);
      if (columnIndex === undefined) {
        columnIndex = sampleIdToIndex.size();
        sampleIdToIndex.set(sample, columnIndex);
      }
      var rowId = new phantasus.Identifier([
        String(tokens[chromosomeColumn]),
        String(tokens[startPositionColumn]),
        String(tokens[endPositionColumn])]);

      var rowIndex = chromosomeStartEndToIndex.get(rowId);
      if (rowIndex === undefined) {
        rowIndex = chromosomeStartEndToIndex.size();
        chromosomeStartEndToIndex.set(rowId, rowIndex);
      }
      var value = parseFloat(String(tokens[valueField]));
      value = isNaN(value) ? value : (phantasus.Log2(value) - 1);
      var matrixRow = matrix[rowIndex];
      if (matrixRow === undefined) {
        matrixRow = [];
        matrix[rowIndex] = matrixRow;
        if (ccfColumnIndex !== undefined) {
          ccfMatrix[rowIndex] = [];
        }
      }
      matrixRow[columnIndex] = value;
      if (ccfColumnIndex !== undefined) {
        ccfMatrix[rowIndex][columnIndex] = parseFloat(tokens[ccfColumnIndex]);
      }
    }
    var dataset = new phantasus.Dataset({
      name: datasetName,
      array: matrix,
      dataType: 'number',
      rows: chromosomeStartEndToIndex.size(),
      columns: sampleIdToIndex.size()
    });

    var columnIds = dataset.getColumnMetadata().add('id');
    sampleIdToIndex.forEach(function (index, id) {
      columnIds.setValue(index, id);
    });

    var chromosomeVector = dataset.getRowMetadata().add('Chromosome');
    var startVector = dataset.getRowMetadata().add('Start_bp');
    var endVector = dataset.getRowMetadata().add('End_bp');
    chromosomeStartEndToIndex.forEach(function (index, id) {
      chromosomeVector.setValue(index, id.getArray()[0]);
      startVector.setValue(index, id.getArray()[1]);
      endVector.setValue(index, id.getArray()[2]);
    });

    if (ccfColumnIndex !== undefined) {
      dataset.addSeries({
        dataType: 'number',
        name: 'ccf',
        array: ccfMatrix
      });
    }

    if (this.regions != null && this.regions.length > 0) {
      dataset = phantasus.SegTabReader.binByRegion(dataset, this.regions);
    }
    return dataset;
  },
  read: function (fileOrUrl, callback) {
    var _this = this;
    var name = phantasus.Util.getBaseFileName(phantasus.Util
      .getFileName(fileOrUrl));
    phantasus.ArrayBufferReader.getArrayBuffer(fileOrUrl, function (err,
                                                                   arrayBuffer) {
      if (err) {
        callback(err);
      } else {
        // try {
        callback(null, _this._read(name, new phantasus.ArrayBufferReader(
          new Uint8Array(arrayBuffer))));
        // } catch (err) {
        // callback(err);
        // }
      }
    });

  }
};

phantasus.TcgaUtil = function () {

};

phantasus.TcgaUtil.DISEASE_STUDIES = {
  'ACC': 'Adrenocortical carcinoma',
  'BLCA': 'Bladder Urothelial Carcinoma',
  'BRCA': 'Breast invasive carcinoma',
  'CESC': 'Cervical squamous cell carcinoma and endocervical adenocarcinoma',
  'CHOL': 'Cholangiocarcinoma',
//	'CNTL': 'Controls',
  'COAD': 'Colon adenocarcinoma',
  'COADREAD': 'Colonrectal adenocarcinoma',
  'DLBC': 'Lymphoid Neoplasm Diffuse Large B-cell Lymphoma',
  'ESCA': 'Esophageal carcinoma ',
//	'FPPP': 'FFPE Pilot Phase II',
  'GBM': 'Glioblastoma multiforme',
  'GBMLGG': 'Glioma',
  'HNSC': 'Head and Neck squamous cell carcinoma',
  'KICH': 'Kidney Chromophobe',
  'KIPAN': 'Pan-Kidney Cohort',
  'KIRC': 'Kidney renal clear cell carcinoma',
  'KIRP': 'Kidney renal papillary cell carcinoma',
  'LAML': 'Acute Myeloid Leukemia',
  'LCML': 'Chronic Myelogenous Leukemia',
  'LGG': 'Brain Lower Grade Glioma',
  'LIHC': 'Liver hepatocellular carcinoma',
  'LUAD': 'Lung adenocarcinoma',
  'LUSC': 'Lung squamous cell carcinoma',
  'MESO': 'Mesothelioma',
//	'MISC': 'Miscellaneous',
  'OV': 'Ovarian serous cystadenocarcinoma',
  'PAAD': 'Pancreatic adenocarcinoma',
  'PCPG': 'Pheochromocytoma and Paraganglioma',
  'PRAD': 'Prostate adenocarcinoma',
  'READ': 'Rectum adenocarcinoma',
  'SARC': 'Sarcoma',
  'SKCM': 'Skin Cutaneous Melanoma',
  'STAD': 'Stomach adenocarcinoma',
  'STES': 'Stomach and Esophageal Carcinoma',
  'TGCT': 'Testicular Germ Cell Tumors',
  'THCA': 'Thyroid carcinoma',
  'THYM': 'Thymoma',
  'UCEC': 'Uterine Corpus Endometrial Carcinoma',
  'UCS': 'Uterine Carcinosarcoma',
  'UVM': 'Uveal Melanoma'
};

phantasus.TcgaUtil.SAMPLE_TYPES = {
  '01': 'Primary solid Tumor',
  '02': 'Recurrent Solid Tumor',
  '03': 'Primary Blood Derived Cancer - Peripheral Blood',
  '04': 'Recurrent Blood Derived Cancer - Bone Marrow',
  '05': 'Additional - New Primary',
  '06': 'Metastatic',
  '07': 'Additional Metastatic',
  '08': 'Human Tumor Original Cells',
  '09': 'Primary Blood Derived Cancer - Bone Marrow',
  '10': 'Blood Derived Normal',
  '11': 'Solid Tissue Normal',
  '12': 'Buccal Cell Normal',
  '13': 'EBV Immortalized Normal',
  '14': 'Bone Marrow Normal',
  '20': 'Control Analyte',
  '40': 'Recurrent Blood Derived Cancer - Peripheral Blood',
  '50': 'Cell Lines',
  '60': 'Primary Xenograft Tissue',
  '61': 'Cell Line Derived Xenograft Tissue'
};

phantasus.TcgaUtil.barcode = function (s) {
  // e.g. TCGA-AC-A23H-01A-11D-A159-09
  // see https://wiki.nci.nih.gov/display/TCGA/TCGA+barcode
  // TCGA, Tissue source site, Study participant, Sample type
  var tokens = s.split('-');
  var id = tokens[2];
  var sampleType;

  if (tokens.length > 3) {
    sampleType = tokens[3];
    if (sampleType.length > 2) {
      sampleType = sampleType.substring(0, 2);
    }
    sampleType = phantasus.TcgaUtil.SAMPLE_TYPES[sampleType];
  } else {
    sampleType = phantasus.TcgaUtil.SAMPLE_TYPES['01'];
  }
  return {
    id: id.toLowerCase(),
    sampleType: sampleType
  };
};

phantasus.TcgaUtil.setIdAndSampleType = function (dataset) {
  var idVector = dataset.getColumnMetadata().get(0);
  var participantId = dataset.getColumnMetadata().add('participant_id');
  var sampleType = dataset.getColumnMetadata().add('sample_type');
  for (var i = 0, size = idVector.size(); i < size; i++) {
    var barcode = phantasus.TcgaUtil.barcode(idVector.getValue(i));
    idVector.setValue(i, barcode.id + '-' + barcode.sampleType);
    sampleType.setValue(i, barcode.sampleType);
    participantId.setValue(i, barcode.id);
  }
};

phantasus.TcgaUtil.getDataset = function (options) {
  var promises = [];
  var datasets = [];
  var returnDeferred = $.Deferred();

  if (options.mrna) {
    // id + type
    var mrna = $.Deferred();
    promises.push(mrna);
    new phantasus.TxtReader().read(options.mrna, function (err, dataset) {
      if (err) {
        console.log('Error reading file:' + err);
      } else {
        datasets.push(dataset);
        phantasus.TcgaUtil.setIdAndSampleType(dataset);
      }
      console.log("mrna promise", datasets);
      mrna.resolve();
    });
  }
  var sigGenesLines;
  if (options.mutation) {
    var mutation = $.Deferred();
    promises.push(mutation);
    new phantasus.MafFileReader().read(options.mutation, function (err, dataset) {
      if (err) {
        // console.log('Error reading file:' + err);
      } else {
        datasets.push(dataset);
        phantasus.TcgaUtil.setIdAndSampleType(dataset);
      }
      mutation.resolve();
    });
    var sigGenesAnnotation = phantasus.Util.readLines(options.sigGenes);
    sigGenesAnnotation.done(function (lines) {
      sigGenesLines = lines;
    });
    promises.push(sigGenesAnnotation);
  }
  if (options.gistic) {
    var gistic = $.Deferred();
    promises.push(gistic);
    new phantasus.GisticReader().read(options.gistic,
      function (err, dataset) {
        if (err) {
          // console.log('Error reading file:' + err);
        } else {
          datasets.push(dataset);
          phantasus.TcgaUtil.setIdAndSampleType(dataset);
        }
        gistic.resolve();
      });

  }
  if (options.gisticGene) {
    var gisticGene = $.Deferred();
    promises.push(gisticGene);

    new phantasus.TxtReader({
      dataColumnStart: 3

    }).read(options.gisticGene, function (err, dataset) {
      if (err) {
        // console.log('Error reading file:' + err);
      } else {
        datasets.push(dataset);
        phantasus.TcgaUtil.setIdAndSampleType(dataset);
      }
      gisticGene.resolve();
    });

  }
  if (options.seg) {
    var seg = $.Deferred();
    promises.push(seg);
    new phantasus.SegTabReader().read(options.seg, function (err, dataset) {
      if (err) {
        // console.log('Error reading file:' + err);
      } else {
        datasets.push(dataset);
        phantasus.TcgaUtil.setIdAndSampleType(dataset);
      }
      seg.resolve();
    });
  }
  if (options.rppa) {
    // id + type
    var rppa = $.Deferred();
    promises.push(rppa);

    new phantasus.TxtReader({dataColumnStart: 2}).read(options.rppa, function (err, dataset) {
      if (err) {
        // console.log('Error reading file:' + err);
      } else {
        datasets.push(dataset);
        phantasus.TcgaUtil.setIdAndSampleType(dataset);
      }

      rppa.resolve();
    });

  }
  if (options.methylation) {
    // id + type
    var methylation = $.Deferred();
    promises.push(methylation);
    new phantasus.TxtReader({}).read(options.methylation, function (
      err,
      dataset) {
      if (err) {
        // console.log('Error reading file:' + err);
      } else {
        datasets.push(dataset);
        phantasus.TcgaUtil.setIdAndSampleType(dataset);
      }
      methylation.resolve();
    });
  }

  var mrnaClustPromise = phantasus.Util.readLines(options.mrnaClust);
  promises.push(mrnaClustPromise);
  var sampleIdToClusterId;
  mrnaClustPromise.done(function (lines) {
    // SampleName cluster silhouetteValue
    // SampleName cluster silhouetteValue
    // TCGA-OR-A5J1-01 1 0.00648776228925048
    sampleIdToClusterId = new phantasus.Map();
    var lineNumber = 0;
    while (lines[lineNumber].indexOf('SampleName') !== -1) {
      lineNumber++;
    }
    var tab = /\t/;
    for (; lineNumber < lines.length; lineNumber++) {
      var tokens = lines[lineNumber].split(tab);
      var barcode = phantasus.TcgaUtil.barcode(tokens[0]);
      sampleIdToClusterId.set(barcode.id + '-' + barcode.sampleType, tokens[1]);
    }
  });
  var annotationCallbacks = [];
  var annotationDef = null;
  if (options.columnAnnotations) {
    // match datasetField: 'participant_id' to fileField: 'patient_id', // e.g. tcga-5l-aat0
    annotationDef = phantasus.DatasetUtil.annotate({
      annotations: options.columnAnnotations,
      isColumns: true
    });
    promises.push(annotationDef);
    annotationDef.done(function (array) {
      annotationCallbacks = array;
    });
  }
  $.when.apply($, promises).then(
    function () {
      var datasetToReturn = null;
      if (datasets.length === 1) {
        var sourceName = datasets[0].getName();
        var sourceVector = datasets[0].getRowMetadata().add(
          'Source');
        for (var i = 0; i < sourceVector.size(); i++) {
          sourceVector.setValue(i, sourceName);
        }
        datasetToReturn = datasets[0];

      } else {
        var maxIndex = 0;
        var maxColumns = datasets[0].getColumnCount();
        // use dataset with most columns as the reference or
        // mutation data
        for (var i = 1; i < datasets.length; i++) {
          if (datasets[i].getColumnCount() > maxColumns) {
            maxColumns = datasets[i].getColumnCount();
            maxIndex = i;
          }
          if (datasets[i].getName() === 'mutations_merged.maf') {
            maxColumns = Number.MAX_VALUE;
            maxIndex = i;
          }
        }
        var datasetIndices = [];
        datasetIndices.push(maxIndex);
        for (var i = 0; i < datasets.length; i++) {
          if (i !== maxIndex) {
            datasetIndices.push(i);
          }
        }

        var joined = new phantasus.JoinedDataset(
          datasets[datasetIndices[0]],
          datasets[datasetIndices[1]], 'id', 'id');
        for (var i = 2; i < datasetIndices.length; i++) {
          joined = new phantasus.JoinedDataset(joined,
            datasets[datasetIndices[i]], 'id', 'id');
        }
        datasetToReturn = joined;
      }

      var clusterIdVector = datasetToReturn.getColumnMetadata().add(
        'mRNAseq_cluster');
      var idVector = datasetToReturn.getColumnMetadata().getByName(
        'id');
      for (var j = 0, size = idVector.size(); j < size; j++) {
        clusterIdVector.setValue(j, sampleIdToClusterId
          .get(idVector.getValue(j)));
      }
      // view in space of mutation sample ids only
      if (options.mutation) {
        var sourceToIndices = phantasus.VectorUtil
          .createValueToIndicesMap(datasetToReturn
            .getRowMetadata().getByName('Source'));
        var mutationDataset = new phantasus.SlicedDatasetView(
          datasetToReturn, sourceToIndices
            .get('mutations_merged.maf'));
        new phantasus.OpenFileTool()
          .annotate(sigGenesLines, mutationDataset, false,
            null, 'id', 'gene', ['q']);
        var qVector = mutationDataset.getRowMetadata().getByName(
          'q');
        var qValueVector = mutationDataset.getRowMetadata()
          .getByName('q_value');
        if (qValueVector == null) {
          qValueVector = mutationDataset.getRowMetadata().add(
            'q_value');
        }
        for (var i = 0, size = qValueVector.size(); i < size; i++) {
          qValueVector.setValue(i, qVector.getValue(i));
        }

        mutationDataset.getRowMetadata().remove(
          phantasus.MetadataUtil.indexOf(mutationDataset
            .getRowMetadata(), 'q'));
      }
      if (annotationDef) {
        annotationCallbacks.forEach(function (f) {
          f(datasetToReturn);
        });
      }
      // console.log("phantasus.TcgaUtil.setIdAndSampleType ::", datasetToReturn);
      phantasus.DatasetUtil.toESSessionPromise(datasetToReturn);
      returnDeferred.resolve(datasetToReturn);
    });
  return returnDeferred;
};

/**
 *
 * @param options.dataRowStart
 * @param options.dataColumnStart
 * @constructor
 */
phantasus.TxtReader = function (options) {
  if (options == null) {
    options = {};
  }
  this.options = options;
};
phantasus.TxtReader.prototype = {
  read: function (fileOrUrl, callback) {
    var _this = this;
    var name = phantasus.Util.getBaseFileName(phantasus.Util
      .getFileName(fileOrUrl));
    phantasus.ArrayBufferReader.getArrayBuffer(fileOrUrl, function (err,
                                                                   arrayBuffer) {
      if (err) {
        callback(err);
      } else {
        try {
          callback(null, _this._read(name,
            new phantasus.ArrayBufferReader(new Uint8Array(
              arrayBuffer))));
        }
        catch (x) {
          callback(x);
        }
      }
    });

  },
  _read: function (datasetName, reader) {
    var dataColumnStart = this.options.dataColumnStart;
    var dataRowStart = this.options.dataRowStart;
    if (dataRowStart == null) {
      dataRowStart = 1;
    }
    var tab = /\t/;
    var header = reader.readLine().trim().split(tab);
    if (dataRowStart > 1) {
      for (var i = 1; i < dataRowStart; i++) {
        reader.readLine(); // skip
      }
    }
    var testLine = null;
    if (dataColumnStart == null) { // try to figure out where data starts by finding 1st
      // numeric column
      testLine = reader.readLine().trim();
      var tokens = testLine.split(tab);
      for (var i = 1; i < tokens.length; i++) {
        var token = tokens[i];
        if (token === '' || token === 'NA' || token === 'NaN' || $.isNumeric(token)) {
          dataColumnStart = i;
          break;
        }
      }

      if (dataColumnStart == null) {
        dataColumnStart = 1;
      }
    }

    var ncols = header.length - dataColumnStart;
    var matrix = [];
    var s;
    var arrayOfRowArrays = [];
    for (var i = 0; i < dataColumnStart; i++) {
      arrayOfRowArrays.push([]);
    }
    if (testLine != null) {
      var array = new Float32Array(ncols);
      matrix.push(array);
      var tokens = testLine.split(tab);
      for (var j = 0; j < dataColumnStart; j++) {
        // row metadata
        arrayOfRowArrays[j].push(phantasus.Util.copyString(tokens[j]));
      }
      for (var j = dataColumnStart, k = 0; k < ncols; j++, k++) {
        var token = tokens[j];
        array[j - dataColumnStart] = parseFloat(token);
      }
    }
    while ((s = reader.readLine()) !== null) {
      s = s.trim();
      if (s !== '') {
        var array = new Float32Array(ncols);
        matrix.push(array);
        var tokens = s.split(tab);
        for (var j = 0; j < dataColumnStart; j++) {
          // row metadata
          arrayOfRowArrays[j].push(phantasus.Util.copyString(tokens[j]));
        }
        for (var j = dataColumnStart, k = 0; k < ncols; j++, k++) {
          var token = tokens[j];
          array[j - dataColumnStart] = parseFloat(token);
        }
      }
    }
    var dataset = new phantasus.Dataset({
      name: datasetName,
      rows: matrix.length,
      columns: ncols,
      array: matrix,
      dataType: 'Float32'
    });

    var columnIds = dataset.getColumnMetadata().add('id');
    for (var i = 0, j = dataColumnStart; i < ncols; i++, j++) {
      columnIds.setValue(i, phantasus.Util.copyString(header[j]));
    }
    var rowIdVector = dataset.getRowMetadata().add('id');
    rowIdVector.array = arrayOfRowArrays[0];
    // add additional row metadata
    for (var i = 1; i < dataColumnStart; i++) {
      var v = dataset.getRowMetadata().add(header[i]);
      v.array = arrayOfRowArrays[i];
    }

    return dataset;
  }
};

phantasus.XlsxDatasetReader = function () {
};
phantasus.XlsxDatasetReader.prototype = {
  read: function (fileOrUrl, callback) {
    var _this = this;
    var name = phantasus.Util.getBaseFileName(phantasus.Util
      .getFileName(fileOrUrl));
    phantasus.ArrayBufferReader.getArrayBuffer(fileOrUrl, function (err,
                                                                   arrayBuffer) {
      if (err) {
        callback(err);
      } else {
        try {
          var data = new Uint8Array(arrayBuffer);
          var arr = [];
          for (var i = 0; i != data.length; ++i) {
            arr[i] = String.fromCharCode(data[i]);
          }
          var bstr = arr.join('');
          _this._read(name, bstr, callback);
        }
        catch (x) {
          callback(x);
        }
      }
    });

  },

  _read: function (datasetName, bstr, callback) {
    phantasus.Util.xlsxTo2dArray({data: bstr}, function (err, lines) {
      var nrows = lines.length - 1;
      var header = lines[0];
      var ncols = header.length - 1;
      var dataset = new phantasus.Dataset({
        name: datasetName,
        rows: nrows,
        columns: ncols
      });
      var columnIds = dataset.getColumnMetadata().add('id');
      for (var j = 1; j <= ncols; j++) {
        columnIds.setValue(j - 1, header[j]);
      }
      var rowIds = dataset.getRowMetadata().add('id');
      for (var i = 1; i < lines.length; i++) {
        var tokens = lines[i];
        rowIds.setValue(i - 1, tokens[0]);
        for (var j = 1; j <= ncols; j++) {
          var token = tokens[j];
          var value = parseFloat(token);
          dataset.setValue(i - 1, j - 1, value);
        }
      }
      callback(null, dataset);
    });

  }
};

phantasus.VectorAdapter = function (v) {
  if (v == null) {
    throw 'vector is null';
  }
  this.v = v;
};
phantasus.VectorAdapter.prototype = {
  setValue: function (i, value) {
    this.v.setValue(i, value);
  },
  getValue: function (i) {
    return this.v.getValue(i);
  },
  getProperties: function () {
    return this.v.getProperties();
  },
  size: function () {
    return this.v.size();
  },
  getName: function () {
    return this.v.getName();
  },
  setName: function (name) {
    this.v.setName(name);
  }
};

/**
 *
 * Creates a new dataset with the specified dimensions. Subclasses must implement getValue and
 * setValue.
 * @param rows {number} The number of rows
 * @param columns {number} The number of columns
 * @implements {phantasus.DatasetInterface}
 * @constructor
 */
phantasus.AbstractDataset = function (rows, columns) {
  this.seriesNames = [];
  this.seriesArrays = [];
  this.seriesDataTypes = [];
  this.rows = rows;
  this.columns = columns;
  this.rowMetadataModel = new phantasus.MetadataModel(rows);
  this.columnMetadataModel = new phantasus.MetadataModel(columns);
};
phantasus.AbstractDataset.prototype = {
  /**
   * @ignore
   * @param metadata
   */
  setRowMetadata: function (metadata) {
    this.rowMetadataModel = metadata;
  },
  /**
   * @ignore
   * @param metadata
   */
  setColumnMetadata: function (metadata) {
    this.columnMetadataModel = metadata;
  },
  /**
   * Returns the name for the given series. Series can be used to store
   * standard error of data points for example.
   *
   * @param seriesIndex
   *            the series
   * @return the series name
   */
  getName: function (seriesIndex) {
    return this.seriesNames[seriesIndex || 0];
  },
  /**
   * Sets the name for the given series. Series can be used to store standard
   * error of data points for example.
   *
   * @param seriesIndex
   *            the series *
   * @param name
   *            the series name
   */
  setName: function (seriesIndex, name) {
    this.seriesNames[seriesIndex || 0] = name;
  },
  /**
   * Gets the row metadata for this dataset.
   *
   * @return the row metadata
   */
  getRowMetadata: function () {
    return this.rowMetadataModel;
  },
  /**
   * Gets the column metadata for this dataset.
   *
   * @return The column metadata
   */
  getColumnMetadata: function () {
    return this.columnMetadataModel;
  },
  /**
   * Returns the number of rows in the dataset.
   *
   * @return the number of rows
   */
  getRowCount: function () {
    return this.rows;
  },
  /**
   * Returns the number of columns in the dataset.
   *
   * @return the number of columns
   */
  getColumnCount: function () {
    return this.columns;
  },
  /**
   * Returns the value at the given row and column for the given series.
   * Series can be used to store standard error of data points for example.
   *
   * @param rowIndex
   *            the row index
   * @param columnIndex
   *            the column index
   * @param seriesIndex
   *            the series index
   * @return the value
   */
  getValue: function (rowIndex, columnIndex, seriesIndex) {
    // not implemented
  },
  /**
   * Sets the value at the given row and column for the given series.
   *
   * @param rowIndex
   *            the row index
   *
   * @param columnIndex
   *            the column index
   * @param value
   *            the value
   * @param seriesIndex
   *            the series index
   *
   */
  setValue: function (rowIndex, columnIndex, value, seriesIndex) {
    // not implemented
  },
  /**
   * Adds the specified series.
   *
   * @param options
   * @param options.name
   *            the series name
   * @param options.dataType
   *            the series data type (e.g. object, Float32, Int8)
   * @return the series index
   */
  addSeries: function (options) {
    // not implemented
  },
  /**
   * Removes the specified series.
   *
   * @param seriesIndex The series index.
   */
  removeSeries: function (seriesIndex) {
    this.seriesArrays.splice(seriesIndex, 1);
    this.seriesNames.splice(seriesIndex, 1);
    this.seriesDataTypes.splice(seriesIndex, 1);
  },
  /**
   * Returns the number of matrix series. Series can be used to store standard
   * error of data points for example.
   *
   * @return the number of series
   */
  getSeriesCount: function () {
    return this.seriesArrays.length;
  },
  /**
   * Returns the data type at the specified series index.
   *
   * @param seriesIndex
   *            the series index
   * @return the series data type (e.g. Number, Float32, Int8)
   */
  getDataType: function (seriesIndex) {
    return this.seriesDataTypes[seriesIndex || 0];
  },
  toString: function () {
    return this.getName();
  }
};

/**
 *
 * Creates a new vector with the given name and size. Subclasses must implement getValue
 *
 * @param {string} name
 *            the vector name
 * @param size {number}
 *            the number of elements in this vector
 * @implements {phantasus.VectorInterface}
 * @constructor
 */
phantasus.AbstractVector = function (name, size) {
  this.name = name;
  this.n = size;
  this.properties = new phantasus.Map();
};

phantasus.AbstractVector.prototype = {
  getValue: function (index) {
    throw new Error('Not implemented');
  },
  getProperties: function () {
    return this.properties;
  },
  size: function () {
    return this.n;
  },
  getName: function () {
    return this.name;
  }
};

phantasus.SignalToNoise = function (list1, list2) {
  var m1 = phantasus.Mean(list1);
  var m2 = phantasus.Mean(list2);
  var s1 = Math.sqrt(phantasus.Variance(list1, m1));
  var s2 = Math.sqrt(phantasus.Variance(list2, m2));
  return (m1 - m2) / (s1 + s2);
};
phantasus.SignalToNoise.toString = function () {
  return 'Signal to noise';
};

phantasus.createSignalToNoiseAdjust = function (percent) {
  percent = percent || 0.2;
  var f = function (list1, list2) {
    var m1 = phantasus.Mean(list1);
    var m2 = phantasus.Mean(list2);
    var s1 = Math.sqrt(phantasus.Variance(list1, m1));
    var s2 = Math.sqrt(phantasus.Variance(list2, m2));
    s1 = phantasus.SignalToNoise.thresholdStandardDeviation(m1, s1, percent);
    s2 = phantasus.SignalToNoise.thresholdStandardDeviation(m2, s2, percent);
    // ensure variance is at least 20% of mean
    return (m1 - m2) / (s1 + s2);
  };
  f.toString = function () {
    return 'Signal to noise (adjust standard deviation)';
  };
  return f;
};

phantasus.SignalToNoise.thresholdStandardDeviation = function (mean,
                                                              standardDeviation, percent) {
  var returnValue = standardDeviation;
  var absMean = Math.abs(mean);
  var minStdev = percent * absMean;
  if (minStdev > standardDeviation) {
    returnValue = minStdev;
  }

  if (returnValue < percent) {
    returnValue = percent;
  }
  return returnValue;
};

phantasus.createContingencyTable = function (listOne, listTwo, groupingValue) {
  if (groupingValue == null || isNaN(groupingValue)) {
    groupingValue = 1;
  }
  var aHit = 0;
  var aMiss = 0;
  for (var j = 0, size = listOne.size(); j < size; j++) {
    var val = listOne.getValue(j);
    if (!isNaN(val)) {
      if (val >= groupingValue) {
        aHit++;
      } else {
        aMiss++;
      }
    }

  }
  var bHit = 0;
  var bMiss = 0;
  for (var j = 0, size = listTwo.size(); j < size; j++) {
    var val = listTwo.getValue(j);
    if (!isNaN(val)) {
      if (val >= groupingValue) {
        bHit++;
      } else {
        bMiss++;
      }
    }

  }
  // listOne=drawn, listTwo=not drawn
  // green=1, red=0
  var N = aHit + aMiss + bHit + bMiss;
  var K = aHit + bHit;
  var n = aHit + aMiss;
  var k = aHit;
  var a = k;
  var b = K - k;
  var c = n - k;
  var d = N + k - n - K;
  return [a, b, c, d];
};
phantasus.FisherExact = function (listOne, listTwo) {
  var abcd = phantasus.createContingencyTable(listOne, listTwo, 1);
  return phantasus.FisherExact.fisherTest(abcd[0], abcd[1], abcd[2], abcd[3]);
};

phantasus.createFisherExact = function (groupingValue) {
  var f = function (listOne, listTwo) {
    var abcd = phantasus.createContingencyTable(listOne, listTwo,
      groupingValue);
    return phantasus.FisherExact.fisherTest(abcd[0], abcd[1], abcd[2],
      abcd[3]);
  };
  return f;

};

/**
 * Computes the hypergeometric probability.
 */
phantasus.FisherExact.phyper = function (a, b, c, d) {
  return Math
    .exp((phantasus.FisherExact.logFactorial(a + b)
      + phantasus.FisherExact.logFactorial(c + d)
      + phantasus.FisherExact.logFactorial(a + c) + phantasus.FisherExact
        .logFactorial(b + d))
      - (phantasus.FisherExact.logFactorial(a)
      + phantasus.FisherExact.logFactorial(b)
      + phantasus.FisherExact.logFactorial(c)
      + phantasus.FisherExact.logFactorial(d) + phantasus.FisherExact
        .logFactorial(a + b + c + d)));

};

phantasus.FisherExact.logFactorials = [0.00000000000000000,
  0.00000000000000000, 0.69314718055994531, 1.79175946922805500,
  3.17805383034794562, 4.78749174278204599, 6.57925121201010100,
  8.52516136106541430, 10.60460290274525023, 12.80182748008146961,
  15.10441257307551530, 17.50230784587388584, 19.98721449566188615,
  22.55216385312342289, 25.19122118273868150, 27.89927138384089157,
  30.67186010608067280, 33.50507345013688888, 36.39544520803305358,
  39.33988418719949404, 42.33561646075348503, 45.38013889847690803,
  48.47118135183522388, 51.60667556776437357, 54.78472939811231919,
  58.00360522298051994, 61.26170176100200198, 64.55753862700633106,
  67.88974313718153498, 71.25703896716800901];
phantasus.FisherExact.logFactorial = function (k) {
  if (k >= 30) { // stirlings approximation
    var C0 = 9.18938533204672742e-01;
    var C1 = 8.33333333333333333e-02;
    var C3 = -2.77777777777777778e-03;
    var C5 = 7.93650793650793651e-04;
    var C7 = -5.95238095238095238e-04;
    var r = 1.0 / k;
    var rr = r * r;
    return (k + 0.5) * Math.log(k) - k + C0 + r
      * (C1 + rr * (C3 + rr * (C5 + rr * C7)));
    // log k! = (k + 1/2)log(k) - k + (1/2)log(2Pi) + stirlingCorrection(k)
  }
  return phantasus.FisherExact.logFactorials[k];
};

phantasus.FisherExact.fisherTest = function (a, b, c, d) {
  // match R 2-sided fisher.test
  var p = phantasus.FisherExact.phyper(a, b, c, d);
  var sum = p;
  for (var _a = 0, n = a + b + c + d; _a <= n; _a++) {
    var _b = a + b - _a;
    var _c = a + c - _a;
    var _d = b + d - _b;
    if (_a !== a && _b >= 0 && _c >= 0 && _d >= 0) {
      var _p = phantasus.FisherExact.phyper(_a, _b, _c, _d);
      if (_p <= p) {
        sum += _p;
      }
    }
  }
  return Math.min(1, sum);
  // var lt = jStat.hypgeom.cdf(a, a + b + c + d, a + b, a + c);
  // var gt = jStat.hypgeom.cdf(b, a + b + c + d, a + b, b + d);
  // return Math.min(1, 2 * Math.min(lt, gt));
};
phantasus.FisherExact.toString = function () {
  return 'Fisher Exact Test';
};

phantasus.FoldChange = function (list1, list2) {
  var m1 = phantasus.Mean(list1);
  var m2 = phantasus.Mean(list2);
  return (m1 / m2);
};
phantasus.FoldChange.toString = function () {
  return 'Fold Change';
};

phantasus.MeanDifference = function (list1, list2) {
  var m1 = phantasus.Mean(list1);
  var m2 = phantasus.Mean(list2);
  return m1 - m2;
};
phantasus.MeanDifference.toString = function () {
  return 'Mean Difference';
};
phantasus.TTest = function (list1, list2) {
  var m1 = phantasus.Mean(list1);
  var m2 = phantasus.Mean(list2);
  var s1 = Math.sqrt(phantasus.Variance(list1, m1));
  var s2 = Math.sqrt(phantasus.Variance(list2, m2));
  var n1 = phantasus.CountNonNaN(list1);
  var n2 = phantasus.CountNonNaN(list2);
  return ((m1 - m2) / Math.sqrt((s1 * s1 / n1) + (s2 * s2 / n2)));
};
phantasus.TTest.toString = function () {
  return 'T-Test';
};
/**
 * Computes approximate degrees of freedom for 2-sample t-test.
 *
 * @param v1 first sample variance
 * @param v2 second sample variance
 * @param n1 first sample n
 * @param n2 second sample n
 * @return approximate degrees of freedom
 */
phantasus.DegreesOfFreedom = function (v1, v2, n1, n2) {
  return (((v1 / n1) + (v2 / n2)) * ((v1 / n1) + (v2 / n2))) / ((v1 * v1) / (n1 * n1 * (n1 - 1.0)) + (v2 * v2) / (n2 * n2 * (n2 - 1.0)));
};

phantasus.Spearman = function (list1, list2) {
  var flist1 = [];
  var flist2 = [];
  for (var i = 0, n = list1.size(); i < n; i++) {
    var v1 = list1.getValue(i);
    var v2 = list2.getValue(i);
    if (isNaN(v1) || isNaN(v2)) {
      continue;
    }
    flist1.push(v1);
    flist2.push(v2);
  }
  var rank1 = phantasus.Ranking(flist1);
  var rank2 = phantasus.Ranking(flist2);
  return phantasus.Pearson(new phantasus.Vector('', rank1.length)
    .setArray(rank1), new phantasus.Vector('', rank2.length)
    .setArray(rank2));
};
phantasus.Spearman.toString = function () {
  return 'Spearman rank correlation';
};
phantasus.WeightedMean = function (weights, values) {
  var numerator = 0;
  var denom = 0;
  for (var i = 0, size = values.size(); i < size; i++) {
    var value = values.getValue(i);
    if (!isNaN(value)) {
      var weight = Math.abs(weights.getValue(i));
      if (!isNaN(weight)) {
        numerator += (weight * value);
        denom += weight;
      }
    }
  }
  return denom === 0 ? NaN : numerator / denom;
};
phantasus.WeightedMean.toString = function () {
  return 'Weighted average';
};

phantasus.createOneMinusMatrixValues = function (dataset) {
  var f = function (listOne, listTwo) {
    return 1 - dataset.getValue(listOne.getIndex(), listTwo.getIndex());
  };
  f.toString = function () {
    return 'One minus matrix values (for a precomputed similarity matrix)';
  };
  return f;
};

phantasus.Pearson = function (listOne, listTwo) {
  var sumx = 0;
  var sumxx = 0;
  var sumy = 0;
  var sumyy = 0;
  var sumxy = 0;
  var N = 0;
  for (var i = 0, size = listOne.size(); i < size; i++) {
    var x = listOne.getValue(i);
    var y = listTwo.getValue(i);
    if (isNaN(x) || isNaN(y)) {
      continue;
    }
    sumx += x;
    sumxx += x * x;
    sumy += y;
    sumyy += y * y;
    sumxy += x * y;
    N++;
  }
  var numr = sumxy - (sumx * sumy / N);
  var denr = Math.sqrt((sumxx - (sumx * sumx / N))
    * (sumyy - (sumy * sumy / N)));
  return denr == 0 ? 1 : numr / denr;
};
phantasus.Pearson.toString = function () {
  return 'Pearson correlation';
};

phantasus.Jaccard = function (listOne, listTwo) {

  var orCount = 0;
  var andCount = 0;
  for (var i = 0, size = listOne.size(); i < size; i++) {
    var xval = listOne.getValue(i);
    var yval = listTwo.getValue(i);
    if (isNaN(xval) || isNaN(yval)) {
      continue;
    }
    var x = xval > 0;
    var y = yval > 0;
    if (x && y) {
      andCount++;
    } else if (x || y) {
      orCount++;
    }
  }
  if (orCount === 0) {
    return 1;
  }
  return 1 - (andCount / orCount);
};

phantasus.Jaccard.toString = function () {
  return 'Jaccard distance';
};

phantasus.Cosine = function (listOne, listTwo) {
  var sumX2 = 0;
  var sumY2 = 0;
  var sumXY = 0;
  for (var i = 0, size = listOne.size(); i < size; i++) {
    var x = listOne.getValue(i);
    var y = listTwo.getValue(i);
    if (isNaN(x) || isNaN(y)) {
      continue;
    }
    sumX2 += x * x;
    sumY2 += y * y;
    sumXY += x * y;
  }
  return (sumXY / Math.sqrt(sumX2 * sumY2));
};

phantasus.Cosine.toString = function () {
  return 'Cosine similarity';
};

phantasus.Euclidean = function (x, y) {
  var dist = 0;
  for (var i = 0, size = x.size(); i < size; ++i) {
    var x_i = x.getValue(i);
    var y_i = y.getValue(i);
    if (isNaN(x_i) || isNaN(y_i)) {
      continue;
    }
    dist += (x_i - y_i) * (x_i - y_i);
  }
  return Math.sqrt(dist);
};
phantasus.Euclidean.toString = function () {
  return 'Euclidean distance';
};
phantasus.OneMinusFunction = function (f) {
  var dist = function (x, y) {
    return 1 - f(x, y);
  };
  dist.toString = function () {
    var s = f.toString();
    return 'One minus ' + s[0].toLowerCase() + s.substring(1);
  };
  return dist;
};

phantasus.LinearRegression = function (xVector, yVector) {
  var sumX = 0;
  var sumY = 0;
  var sumXX = 0;
  var sumXY = 0;
  var count = 0;
  for (var i = 0, size = xVector.size(); i < size; i++) {
    var x = xVector.getValue(i);
    var y = yVector.getValue(i);
    if (!isNaN(x) && !isNaN(y)) {
      sumX += x;
      sumY += y;
      sumXX += x * x;
      sumXY += x * y;
      count++;
    }
  }

  var m = ((count * sumXY) - (sumX * sumY)) /
    ((count * sumXX) - (sumX * sumX));
  var b = (sumY / count) - ((m * sumX) / count);
  return {
    m: m,
    b: b
  };
};

phantasus.KendallsCorrelation = function (x, y) {

  /**
   * Returns the sum of the number from 1 .. n according to Gauss' summation formula:
   * \[ \sum\limits_{k=1}^n k = \frac{n(n + 1)}{2} \]
   *
   * @param n the summation end
   * @return the sum of the number from 1 to n
   */
  function sum(n) {
    return n * (n + 1) / 2;
  }

  var xArray = [];
  var yArray = [];
  for (var i = 0, size = x.size(); i < size; ++i) {
    var x_i = x.getValue(i);
    var y_i = y.getValue(i);
    if (isNaN(x_i) || isNaN(y_i)) {
      continue;
    }
    xArray.push(x_i);
    yArray.push(y_i);
  }
  var n = xArray.length;
  var numPairs = sum(n - 1);
  var pairs = [];
  for (var i = 0; i < n; i++) {
    pairs[i] = [xArray[i], yArray[i]];
  }
  pairs.sort(function (pair1, pair2) {
    var a = pair1[0];
    var b = pair2[0];
    var compareFirst = (a === b ? 0 : (a < b ? -1 : 1));
    if (compareFirst !== 0) {
      return compareFirst;
    }
    a = pair1[1];
    b = pair2[1];
    return (a === b ? 0 : (a < b ? -1 : 1));
  });

  var tiedXPairs = 0;
  var tiedXYPairs = 0;
  var consecutiveXTies = 1;
  var consecutiveXYTies = 1;
  var prev = pairs[0];
  for (var i = 1; i < n; i++) {
    var curr = pairs[i];
    if (curr[0] === prev[0]) {
      consecutiveXTies++;
      if (curr[1] === prev[1]) {
        consecutiveXYTies++;
      } else {
        tiedXYPairs += sum(consecutiveXYTies - 1);
        consecutiveXYTies = 1;
      }
    } else {
      tiedXPairs += sum(consecutiveXTies - 1);
      consecutiveXTies = 1;
      tiedXYPairs += sum(consecutiveXYTies - 1);
      consecutiveXYTies = 1;
    }
    prev = curr;
  }
  tiedXPairs += sum(consecutiveXTies - 1);
  tiedXYPairs += sum(consecutiveXYTies - 1);
  var swaps = 0;
  var pairsDestination = [];
  for (var segmentSize = 1; segmentSize < n; segmentSize <<= 1) {
    for (var offset = 0; offset < n; offset += 2 * segmentSize) {
      var i = offset;
      var iEnd = Math.min(i + segmentSize, n);
      var j = iEnd;
      var jEnd = Math.min(j + segmentSize, n);
      var copyLocation = offset;
      while (i < iEnd || j < jEnd) {
        if (i < iEnd) {
          if (j < jEnd) {
            var c = (pairs[i][1] === pairs[j][1] ? 0 : (pairs[i][1] < pairs[j][1] ? -1 : 1));
            if (c <= 0) {
              pairsDestination[copyLocation] = pairs[i];
              i++;
            } else {
              pairsDestination[copyLocation] = pairs[j];
              j++;
              swaps += iEnd - i;
            }
          } else {
            pairsDestination[copyLocation] = pairs[i];
            i++;
          }
        } else {
          pairsDestination[copyLocation] = pairs[j];
          j++;
        }
        copyLocation++;
      }
    }
    var pairsTemp = pairs;
    pairs = pairsDestination;
    pairsDestination = pairsTemp;
  }

  var tiedYPairs = 0;
  var consecutiveYTies = 1;
  prev = pairs[0];
  for (var i = 1; i < n; i++) {
    var curr = pairs[i];
    if (curr[1] === prev[1]) {
      consecutiveYTies++;
    } else {
      tiedYPairs += sum(consecutiveYTies - 1);
      consecutiveYTies = 1;
    }
    prev = curr;
  }
  tiedYPairs += sum(consecutiveYTies - 1);

  var concordantMinusDiscordant = numPairs - tiedXPairs - tiedYPairs + tiedXYPairs - 2 * swaps;
  var nonTiedPairsMultiplied = (numPairs - tiedXPairs) * (numPairs - tiedYPairs);
  return concordantMinusDiscordant / Math.sqrt(nonTiedPairsMultiplied);
};
phantasus.KendallsCorrelation.toString = function () {
  return 'Kendall\'s correlation';
};

/**
 * Creates a new computed vector with the given name and size.
 *
 * @param name
 *            the vector name
 * @param size
 *            the number of elements in this vector
 * @param callback {Function} that takes an index and returns the value at the specified index
 * @constructor
 */
phantasus.ComputedVector = function (name, size, callback) {
  phantasus.AbstractVector.call(this, name, size);
  this.callback = callback;
};

phantasus.ComputedVector.prototype = {
  getValue: function (index) {
    return this.callback(index);
  }
};
phantasus.Util.extend(phantasus.ComputedVector, phantasus.AbstractVector);

phantasus.DatasetAdapter = function (dataset, rowMetadata, columnMetadata) {
  if (dataset == null) {
    throw 'dataset is null';
  }
  this.dataset = dataset;
  this.rowMetadata = rowMetadata || dataset.getRowMetadata();
  this.columnMetadata = columnMetadata || dataset.getColumnMetadata();

};
phantasus.DatasetAdapter.prototype = {
  getDataset: function () {
    return this.dataset;
  },
  getName: function (seriesIndex) {
    return this.dataset.getName(seriesIndex);
  },
  setName: function (seriesIndex, name) {
    this.dataset.setName(seriesIndex, name);
  },
  getRowMetadata: function () {
    return this.rowMetadata;
  },
  getColumnMetadata: function () {
    return this.columnMetadata;
  },
  getRowCount: function () {
    return this.dataset.getRowCount();
  },
  getColumnCount: function () {
    return this.dataset.getColumnCount();
  },
  getValue: function (rowIndex, columnIndex, seriesIndex) {
    return this.dataset.getValue(rowIndex, columnIndex, seriesIndex);
  },
  setValue: function (rowIndex, columnIndex, value, seriesIndex) {
    this.dataset.setValue(rowIndex, columnIndex, value, seriesIndex);
  },
  addSeries: function (options) {
    return this.dataset.addSeries(options);
  },
  removeSeries: function (seriesIndex) {
    this.dataset.removeSeries(seriesIndex);
  },
  getSeriesCount: function () {
    return this.dataset.getSeriesCount();
  },
  getDataType: function (seriesIndex) {
    return this.dataset.getDataType(seriesIndex);
  },
  toString: function () {
    return this.dataset.toString();
  },
  getESSession: function () {
    return this.dataset.getESSession();
  },
  setESSession: function (esSession) {
    this.dataset.setESSession(esSession);
  },
  getESVariable: function () {
    return this.dataset.getESVariable();
  },
  setESVariable: function (variable) {
    this.dataset.setESVariable(variable);
  }
};

phantasus.DatasetColumnView = function (dataset) {
  this.dataset = dataset;
  this.columnIndex = 0;
  this.seriesIndex = 0;
};
phantasus.DatasetColumnView.prototype = {
  columnIndex: -1,
  size: function () {
    return this.dataset.getRowCount();
  },
  getValue: function (rowIndex) {
    return this.dataset.getValue(rowIndex, this.columnIndex,
      this.seriesIndex);
  },
  setIndex: function (newColumnIndex) {
    this.columnIndex = newColumnIndex;
    return this;
  },
  setSeriesIndex: function (seriesIndex) {
    this.seriesIndex = seriesIndex;
    return this;
  }
};

/**
 * The interface for a dataset consisting of a two-dimensional matrix of
 * values. A dataset may also optionally contain one or more series of
 * two-dimensional matrices. A dataset also has metadata associated with each
 * row and column.
 *
 * @interface phantasus.DatasetInterface
 */

/**
 * Returns the name for the given series. Series can be used to store
 * standard error of data points for example.
 *
 * @function
 * @name phantasus.DatasetInterface#getName
 * @param seriesIndex {number} the series
 * @return {string} the series name
 */

/**
 * Sets the name for the given series. Series can be used to store standard
 * error of data points for example.
 *
 * @function
 * @name phantasus.DatasetInterface#setName
 * @param seriesIndex {number} the series
 * @param name {string} the series name
 */

/**
 * Gets the row metadata for this dataset.
 *
 * @function
 * @name phantasus.DatasetInterface#getRowMetadata
 * @return {phantasus.MetadataModelInterface} the row metadata
 */

/**
 * Gets the column metadata for this dataset.
 *
 * @function
 * @name phantasus.DatasetInterface#getColumnMetadata
 * @return {phantasus.MetadataModelInterface} The column metadata
 */

/**
 * Returns the number of rows in the dataset.
 *
 * @function
 * @name phantasus.DatasetInterface#getRowCount
 * @return {number} the number of rows
 */

/**
 * Returns the number of columns in the dataset.
 *
 * @function
 * @name phantasus.DatasetInterface#getColumnCount
 * @return {number} the number of columns
 */

/**
 * Returns the value at the given row and column for the given series.
 * Series can be used to store standard error of data points for example.
 *
 * @function
 * @name phantasus.DatasetInterface#getValue
 * @param rowIndex {number} the row index
 * @param columnIndex {number} the column index
 * @param seriesIndex {number} the series index
 * @return the value
 */

/**
 * Sets the value at the given row and column for the given series.
 *
 * @function
 * @name phantasus.DatasetInterface#setValue
 * @param rowIndex {number} the row index
 * @param columnIndex {number} the column index
 * @param value the value
 * @param seriesIndex {number} the series index
 */

/**
 * Adds the specified series.
 *
 * @function
 * @name phantasus.DatasetInterface#addSeries
 * @param options.name {string} the series name
 * @param options.dataType {string} the series data type (e.g. object, Float32, Int8)
 * @return {number} the series index
 */

/**
 * Removes the specified series.
 *
 * @function
 * @name phantasus.DatasetInterface#removeSeries
 * @param seriesIndex {number} The series index.
 */

/**
 * Returns the number of matrix series. Series can be used to store standard
 * error of data points for example.
 *
 * @function
 * @name phantasus.DatasetInterface#getSeriesCount
 * @return {number} the number of series
 */

/**
 * Returns the data type at the specified series index.
 *
 * @function
 * @name phantasus.DatasetInterface#getDataType
 * @param seriesIndex {number} the series index
 * @return {string} the series data type (e.g. Number, Float32, Int8)
 */


phantasus.DatasetRowView = function (dataset) {
  this.dataset = dataset;
  this.index = 0;
  this.seriesIndex = 0;
};
phantasus.DatasetRowView.prototype = {
  size: function () {
    return this.dataset.getColumnCount();
  },
  getIndex: function () {
    return this.index;
  },
  getValue: function (columnIndex) {
    return this.dataset.getValue(this.index, columnIndex, this.seriesIndex);
  },
  setIndex: function (newRowIndex) {
    this.index = newRowIndex;
    return this;
  },
  setSeriesIndex: function (seriesIndex) {
    this.seriesIndex = seriesIndex;
    return this;
  },
  setDataset: function (dataset) {
    this.dataset = dataset;
    return this;
  }
};

phantasus.DatasetSeriesView = function (dataset, seriesIndices) {
  phantasus.DatasetAdapter.call(this, dataset);
  this.seriesIndices = seriesIndices;
};
phantasus.DatasetSeriesView.prototype = {
  getValue: function (i, j, seriesIndex) {
    seriesIndex = seriesIndex || 0;
    return this.dataset.getValue(i, j, this.seriesIndices[seriesIndex]);
  },
  setValue: function (i, j, value, seriesIndex) {
    seriesIndex = seriesIndex || 0;
    this.dataset.setValue(i, j, value, this.seriesIndices[seriesIndex]);
  },
  getName: function (seriesIndex) {
    seriesIndex = seriesIndex || 0;
    return this.dataset.getName(this.seriesIndices[seriesIndex]);
  },
  setName: function (seriesIndex, name) {
    seriesIndex = seriesIndex || 0;
    this.dataset.setName(this.seriesIndices[seriesIndex], name);
  },
  addSeries: function (options) {
    var index = this.dataset.addSeries(options);
    this.seriesIndices.push(index);
    return index;
  },
  getSeriesCount: function () {
    return this.seriesIndices.length;
  },
  toString: function () {
    return this.getName();
  }
};
phantasus.Util.extend(phantasus.DatasetSeriesView, phantasus.DatasetAdapter);

/**
 * Static utilities for phantasus.DatasetInterface instances
 *
 * @class phantasus.DatasetUtil
 */
phantasus.DatasetUtil = function () {
};
phantasus.DatasetUtil.min = function (dataset, seriesIndex) {
  seriesIndex = seriesIndex || 0;
  var min = Number.MAX_VALUE;
  for (var i = 0, rows = dataset.getRowCount(); i < rows; i++) {
    for (var j = 0, columns = dataset.getColumnCount(); j < columns; j++) {
      var d = dataset.getValue(i, j, seriesIndex);
      if (isNaN(d)) {
        continue;
      }
      min = Math.min(min, d);
    }
  }
  return min;
};
phantasus.DatasetUtil.slicedView = function (dataset, rows, columns) {
  return new phantasus.SlicedDatasetView(dataset, rows, columns);
};
phantasus.DatasetUtil.transposedView = function (dataset) {
  return dataset instanceof phantasus.TransposedDatasetView ? dataset
    .getDataset() : new phantasus.TransposedDatasetView(dataset);
};
phantasus.DatasetUtil.max = function (dataset, seriesIndex) {
  seriesIndex = seriesIndex || 0;
  var max = -Number.MAX_VALUE;
  for (var i = 0, rows = dataset.getRowCount(); i < rows; i++) {
    for (var j = 0, columns = dataset.getColumnCount(); j < columns; j++) {
      var d = dataset.getValue(i, j, seriesIndex);
      if (isNaN(d)) {
        continue;
      }
      max = Math.max(max, d);
    }
  }
  return max;
};

phantasus.DatasetUtil.getDatasetReader = function (ext, options) {
  if (options == null) {
    options = {};
  }
  var datasetReader = null;
  if (ext === 'maf') {
    datasetReader = new phantasus.MafFileReader();
    if (options && options.mafGeneFilter) {
      datasetReader.setGeneFilter(options.mafGeneFilter);
    }
  } else if (ext === 'gct') {
    datasetReader = new phantasus.GctReader();
    // datasetReader = new phantasus.StreamingGctReader();
  } else if (ext === 'gmt') {
    datasetReader = new phantasus.GmtDatasetReader();
  } else if (ext === 'xlsx' || ext === 'xls') {
    datasetReader = options.interactive ? new phantasus.Array2dReaderInteractive() : new phantasus.XlsxDatasetReader();
  } else if (ext === 'segtab' || ext === 'seg') {
    datasetReader = new phantasus.SegTabReader();
    if (options && options.regions) {
      datasetReader.setRegions(options.regions);
    }
  } else if (ext === 'txt' || ext === 'tsv' || ext === 'csv') {
    datasetReader = options.interactive ? new phantasus.Array2dReaderInteractive() : new phantasus.TxtReader();
  } else if (ext === 'json') {
    datasetReader = new phantasus.JsonDatasetReader();
  } else if (ext === 'gct') {
    datasetReader = new phantasus.GctReader();
  }
  return datasetReader;
};

phantasus.DatasetUtil.readDatasetArray = function (datasets) {
  var retDef = $.Deferred();
  var loadedDatasets = [];
  var promises = [];
  _.each(datasets, function (url, i) {
    var p = phantasus.DatasetUtil.read(url);
    p.index = i;
    p.done(function (dataset) {
      loadedDatasets[this.index] = dataset;
    });
    p.fail(function (err) {
      var message = [
        'Error opening ' + phantasus.Util
          .getFileName(url) + '.'];
      if (err.message) {
        message.push('<br />Cause: ');
        message.push(err.message);
      }
      retDef.reject(message.join(''));

    });
    promises.push(p);
  });
  if (promises.length === 0) {
    retDef.reject('No datasets specified.');
  }

  $.when
    .apply($, promises)
    .then(
      function () {
        retDef.resolve(phantasus.DatasetUtil.join(loadedDatasets, 'id'));
      });
  return retDef;
};
/**
 * Annotate a dataset from external file or text.
 *
 * @param options.annotations -
 *            Array of file, datasetField, and fileField, and transposed.
 * @param options.isColumns
 *            Whether to annotate columns
 *
 * @return A jQuery Deferred object that resolves to an array of functions to
 *         execute with a dataset parameter.
 */
phantasus.DatasetUtil.annotate = function (options) {
  var retDef = $.Deferred();
  var promises = [];
  var functions = [];
  var isColumns = options.isColumns;
  _.each(options.annotations, function (ann, annotationIndex) {
    if (phantasus.Util.isArray(ann.file)) { // already parsed text
      functions[annotationIndex] = function (dataset) {
        new phantasus.OpenFileTool().annotate(ann.file, dataset,
          isColumns, null, ann.datasetField, ann.fileField,
          ann.include);
      };
    } else {
      var result = phantasus.Util.readLines(ann.file);
      var fileName = phantasus.Util.getFileName(ann.file);
      var deferred = $.Deferred();
      promises.push(deferred);
      result.fail(function (message) {
        deferred.reject(message);
      });
      result.done(function (lines) {
        if (phantasus.Util.endsWith(fileName, '.gmt')) {
          var sets = new phantasus.GmtReader().parseLines(lines);
          functions[annotationIndex] = function (dataset) {
            new phantasus.OpenFileTool().annotate(null, dataset,
              isColumns, sets, ann.datasetField,
              ann.fileField);
          };
          deferred.resolve();
        } else if (phantasus.Util.endsWith(fileName, '.cls')) {
          functions[annotationIndex] = function (dataset) {
            new phantasus.OpenFileTool().annotateCls(null, dataset,
              fileName, isColumns, lines);
          };
          deferred.resolve();
        } else {
          functions[annotationIndex] = function (dataset) {
            new phantasus.OpenFileTool().annotate(lines, dataset,
              isColumns, null, ann.datasetField,
              ann.fileField, ann.include, ann.transposed);
          };
          deferred.resolve();
        }
      });
    }
  });
  $.when.apply($, promises).then(function () {
    retDef.resolve(functions);
  });
  return retDef;
};
/**
 * Reads a dataset at the specified URL or file
 * @param fileOrUrl
 *            a File or URL
 * @param options.background
 * @params options.interactive
 * @params options.extension
 * @return A promise that resolves to phantasus.DatasetInterface
 */
phantasus.DatasetUtil.read = function (fileOrUrl, options) {
  if (fileOrUrl == null) {
    throw 'File is null';
  }
  if (options == null) {
    options = {};
  }
  var isFile = phantasus.Util.isFile(fileOrUrl);
  var isString = phantasus.Util.isString(fileOrUrl);
  var ext = options.extension ? options.extension : phantasus.Util.getExtension(phantasus.Util.getFileName(fileOrUrl));
  var datasetReader;
  var str = fileOrUrl.toString();

  if (options.isGEO) {
    datasetReader = new phantasus.GeoReader();
  }
  else if (options.preloaded) {
    datasetReader = new phantasus.PreloadedReader();
    fileOrUrl = {
      name: fileOrUrl,
      exactName: options.exactName
    }
  }
  else if (ext === '' && str != null && str.indexOf('blob:') === 0) {
    datasetReader = new phantasus.TxtReader(); // copy from clipboard
  } else {
    datasetReader = phantasus.DatasetUtil.getDatasetReader(ext, options);
    if (datasetReader == null) {
      datasetReader = isFile ? (options.interactive ? new phantasus.Array2dReaderInteractive() : new phantasus.TxtReader()) : new phantasus.GctReader();
    }
  }

  // console.log(typeof datasetReader);

  if (isString || isFile) { // URL or file
    var deferred = $.Deferred();
    if (options.background) {
      var path = phantasus.Util.getScriptPath();
      var blob = new Blob(
        [
          'self.onmessage = function(e) {'
          + 'importScripts(e.data.path);'
          + 'var ext = phantasus.Util.getExtension(phantasus.Util'
          + '.getFileName(e.data.fileOrUrl));'
          + 'var datasetReader = phantasus.DatasetUtil.getDatasetReader(ext,'
          + '	e.data.options);'
          + 'datasetReader.read(e.data.fileOrUrl, function(err,dataset) {'
          + '	self.postMessage(dataset);' + '	});' + '}']);

      var blobURL = window.URL.createObjectURL(blob);
      var worker = new Worker(blobURL);
      worker.addEventListener('message', function (e) {
        deferred.resolve(phantasus.Dataset.fromJSON(e.data));
        window.URL.revokeObjectURL(blobURL);
      }, false);
      // start the worker
      worker.postMessage({
        path: path,
        fileOrUrl: fileOrUrl,
        options: options
      });

    } else {
      datasetReader.read(fileOrUrl, function (err, dataset) {
        if (err) {
          deferred.reject(err);
        } else {

          // console.log(dataset);
          // console.log('ready to resolve with', dataset);
          deferred.resolve(dataset);
          if (!options.isGEO && !options.preloaded) {
            phantasus.DatasetUtil.toESSessionPromise({dataset: dataset});
          }
        }
      });

    }
    var pr = deferred.promise();
    // override toString so can determine file name
    pr.toString = function () {
      return '' + fileOrUrl;
    };
    return pr;
  } else if (typeof fileOrUrl.done === 'function') { // assume it's a
    // deferred
    return fileOrUrl;
  } else { // it's already a dataset?
    var deferred = $.Deferred();
    if (fileOrUrl.getRowCount) {
      deferred.resolve(fileOrUrl);
    } else {
      deferred.resolve(phantasus.Dataset.fromJSON(fileOrUrl));
    }
    return deferred.promise();
  }

};

/**
 * @param dataset
 *            The dataset to convert to an array
 * @param options.columns
 *            An array of column indices to include from the dataset
 * @param options.columnFields
 *            An array of field names to use in the returned objects that
 *            correspond to the column indices in the dataset
 * @param options.metadataFields
 *            An array of row metadata fields to include from the dataset
 *
 */
phantasus.DatasetUtil.toObjectArray = function (dataset, options) {
  var columns = options.columns || [0];
  var columnFields = options.columnFields || ['value'];
  if (columnFields.length !== columns.length) {
    throw 'columns.length !== columnFields.length';
  }
  var metadataFields = options.metadataFields;
  // grab all of the headers and filter the meta data vectors in the dataset
  // down
  // to the ones specified in metaFields. If metaFields is not passed, take
  // all metadata
  var rowMetadata = dataset.getRowMetadata();
  if (!metadataFields) {
    metadataFields = phantasus.MetadataUtil.getMetadataNames(rowMetadata);
  }
  var vectors = phantasus.MetadataUtil.getVectors(rowMetadata, metadataFields);
  // build an object that contains the matrix values for the given columns
  // along
  // with any metadata
  var array = [];
  for (var i = 0; i < dataset.getRowCount(); i++) {
    var obj = {};
    for (var j = 0; j < columns.length; j++) {
      obj[columnFields[j]] = dataset.getValue(i, columns[j]);
    }
    for (var j = 0; j < vectors.length; j++) {
      obj[vectors[j].getName()] = vectors[j].getValue(i);
    }
    array.push(obj);
  }
  return array;
};
phantasus.DatasetUtil.fixL1K = function (dataset) {
  var names = {
    'cell_id': 'Cell Line',
    'pert_idose': 'Dose (\u00B5M)',
    'pert_iname': 'Name',
    'pert_itime': 'Time (hr)',
    'distil_ss': 'Signature Strength',
    'pert_type': 'Type',
    'cell_lineage': 'Lineage',
    'cell_histology': 'Histology',
    'cell_type': 'Cell Type'
  };
  var fixNames = function (metadata) {
    for (var i = 0, count = metadata.getMetadataCount(); i < count; i++) {
      var v = metadata.get(i);
      var name = v.getName();
      var mapped = names[name];
      if (mapped) {
        v.setName(mapped);
      }
    }
  };
  fixNames(dataset.getRowMetadata());
  fixNames(dataset.getColumnMetadata());
  var fix666 = function (metadata) {
    for (var i = 0, count = metadata.getMetadataCount(); i < count; i++) {
      var v = metadata.get(i);
      if (v.getName() == 'Dose (\u00B5M)') { // convert to number
        for (var j = 0, size = v.size(); j < size; j++) {
          var value = v.getValue(j);
          if (value != null) {
            v.setValue(j, parseFloat(value));
          }
        }
      }
      var isNumber = false;
      for (var j = 0, size = v.size(); j < size; j++) {
        var value = v.getValue(j);
        if (value != null) {
          isNumber = _.isNumber(value);
          break;
        }
      }
      var newValue = isNumber || v.getName() == 'Dose (\u00B5M)' ? 0 : '';
      for (var j = 0, size = v.size(); j < size; j++) {
        var value = v.getValue(j);
        if (value != null && value == '-666') {
          v.setValue(j, newValue);
        }
      }
    }
  };
  fix666(dataset.getRowMetadata());
  fix666(dataset.getColumnMetadata());
  var fixCommas = function (metadata) {
    var regex = /(,)([^ ])/g;
    _.each(['Lineage', 'Histology'], function (name) {
      var v = metadata.getByName(name);
      if (v != null) {
        for (var i = 0, size = v.size(); i < size; i++) {
          var val = v.getValue(i);
          if (val) {
            v.setValue(i, val.replace(regex, ', $2'));
          }
        }
      }
    });
  };
  fixCommas(dataset.getRowMetadata());
  fixCommas(dataset.getColumnMetadata());
};
phantasus.DatasetUtil.geneSetsToDataset = function (name, sets) {
  var uniqueIds = new phantasus.Map();
  for (var i = 0, length = sets.length; i < length; i++) {
    var ids = sets[i].ids;
    for (var j = 0, nIds = ids.length; j < nIds; j++) {
      uniqueIds.set(ids[j], 1);
    }
  }
  var uniqueIdsArray = uniqueIds.keys();
  var dataset = new phantasus.Dataset({
    name: name,
    rows: uniqueIdsArray.length,
    columns: sets.length
  });
  var columnIds = dataset.getColumnMetadata().add('id');
  for (var i = 0, length = sets.length; i < length; i++) {
    columnIds.setValue(i, sets[i].name);
  }
  var rowIds = dataset.getRowMetadata().add('id');
  for (var i = 0, size = uniqueIdsArray.length; i < size; i++) {
    rowIds.setValue(i, uniqueIdsArray[i]);
  }
  var rowIdToIndex = phantasus.VectorUtil.createValueToIndexMap(rowIds);
  for (var i = 0, length = sets.length; i < length; i++) {
    var ids = sets[i].ids;
    for (var j = 0, nIds = ids.length; j < nIds; j++) {
      dataset.setValue(rowIdToIndex.get(ids[j]), i, 1);
    }
  }
  return dataset;
};

phantasus.DatasetUtil.getRootDataset = function (dataset) {
  while (dataset.getDataset) {
    dataset = dataset.getDataset();
  }
  return dataset;
};

phantasus.DatasetUtil.getSeriesIndex = function (dataset, name) {
  for (var i = 0, nseries = dataset.getSeriesCount(); i < nseries; i++) {
    if (name === dataset.getName(i)) {
      return i;
    }
  }
  return -1;
};
phantasus.DatasetUtil.getSeriesNames = function (dataset) {
  var names = [];
  for (var i = 0, nseries = dataset.getSeriesCount(); i < nseries; i++) {
    names.push(dataset.getName(i));
  }
  // names.sort(function (a, b) {
  // 	a = a.toLowerCase();
  // 	b = b.toLowerCase();
  // 	return (a < b ? -1 : (a === b ? 0 : 1));
  // });
  return names;
};

/**
 * Search dataset values.
 *
 * @param options.dataset
 *      The dataset
 * @param options.text
 *            Search text
 * @param options.defaultMatchMode
 *            'exact' or 'contains'
 * @param options.matchAllPredicates Whether to match all predicates
 * @return Set of matching indices.
 *
 */
phantasus.DatasetUtil.searchValues = function (options) {
  if (text === '') {
    return;
  }
  var dataset = options.dataset;
  var text = options.text;
  var tokens = phantasus.Util.getAutocompleteTokens(text);
  if (tokens.length == 0) {
    return;
  }
  var predicates = phantasus.Util.createSearchPredicates({
    tokens: tokens,
    defaultMatchMode: options.defaultMatchMode
  });
  var matchAllPredicates = options.matchAllPredicates === true;
  var npredicates = predicates.length;
  var viewIndices = new phantasus.Set();

  function isMatch(object, toObject, predicate) {
    if (object != null) {
      if (toObject) {
        var filterColumnName = predicate.getField();
        if (filterColumnName != null) {
          var value = object[filterColumnName];
          return predicate.accept(value);
        } else { // try all fields
          for (var name in object) {
            var value = object[name];
            return predicate.accept(value);
          }
        }
      } else {
        var filterColumnName = predicate.getField();
        if (filterColumnName == null || filterColumnName === dataset.getName(k)) {
          return predicate.accept(object);

        }
      }
    }
  }

  for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
    for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
      var matches = false;
      itemSearch:
        if (matchAllPredicates) {
          matches = true;
          for (var p = 0; p < npredicates; p++) {
            var predicate = predicates[p];
            var pmatch = false;
            for (var k = 0, nseries = dataset.getSeriesCount(); k < nseries; k++) {
              var element = dataset.getValue(i, j, k);
              var isObject = element != null && element.toObject != null;
              if (isObject) {
                element = element.toObject();
              }
              if (isMatch(element, isObject, predicate)) {
                pmatch = true;
                break;
              }
            }
            if (!pmatch) {
              matches = false;
              break itemSearch;
            }
          }
        } else {
          for (var k = 0, nseries = dataset.getSeriesCount(); k < nseries; k++) {
            var element = dataset.getValue(i, j, k);
            var isObject = element != null && element.toObject != null;
            if (isObject) {
              element = element.toObject();
            }
            for (var p = 0; p < npredicates; p++) {
              var predicate = predicates[p];
              if (isMatch(element, isObject, predicate)) {
                matches = true;
                break itemSearch;
              }
            }
          }
        }

      if (matches) {
        viewIndices
          .add(new phantasus.Identifier(
            [i, j]));
      }
    }
  }
  return viewIndices;

};

/**
 * Search dataset values.
 */
phantasus.DatasetUtil.autocompleteValues = function (dataset) {
  return function (tokens, cb) {

    var token = tokens != null && tokens.length > 0 ? tokens[tokens.selectionStartIndex]
      : '';
    token = $.trim(token);
    var seriesIndices = [];
    for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
      for (var k = 0, nseries = dataset.getSeriesCount(); k < nseries; k++) {
        if (dataset.getDataType(i, k) === 'Number') {
          seriesIndices.push([i, k]);
        }
      }
    }
    if (seriesIndices.length === 0) {
      return cb();
    }
    var _val; // first non-null value
    elementSearch: for (var k = 0, nseries = seriesIndices.length; k < nseries; k++) {
      var pair = seriesIndices[k];
      for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
        var element = dataset.getValue(pair[0], j, pair[1]);
        if (element != null && element.toObject) {
          _val = element.toObject();
          break elementSearch;
        }
      }
    }
    var matches = [];
    var fields = _val == null ? [] : _.keys(_val);
    if (token === '') {
      fields.sort(function (a, b) {
        return (a === b ? 0 : (a < b ? -1 : 1));
      });
      fields.forEach(function (field) {
        matches.push({
          value: field + ':',
          label: '<span style="font-weight:300;">' + field
          + ':</span>',
          show: true
        });
      });
      return cb(matches);
    }

    var field = null;
    var semi = token.indexOf(':');
    if (semi > 0) { // field search?
      if (token.charCodeAt(semi - 1) !== 92) { // \:
        var possibleField = $.trim(token.substring(0, semi));
        if (possibleField.length > 0 && possibleField[0] === '"'
          && possibleField[token.length - 1] === '"') {
          possibleField = possibleField.substring(1,
            possibleField.length - 1);
        }
        var index = fields.indexOf(possibleField);
        if (index !== -1) {
          token = $.trim(token.substring(semi + 1));
          field = possibleField;
        }
      }

    }

    var set = new phantasus.Set();
    // regex used to determine if a string starts with substring `q`
    var regex = new RegExp('^' + phantasus.Util.escapeRegex(token), 'i');
    // iterate through the pool of strings and for any string that
    // contains the substring `q`, add it to the `matches` array
    var max = 10;

    loop: for (var k = 0, nseries = seriesIndices.length; k < nseries; k++) {
      var pair = seriesIndices[k];
      for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
        var element = dataset.getValue(pair[0], j, pair[1]);
        if (element && element.toObject) {
          var object = element.toObject();
          if (field !== null) {
            var val = object[field];
            if (val != null) {
              var id = new phantasus.Identifier([val, field]);
              if (!set.has(id) && regex.test(val)) {
                set.add(id);
                if (set.size() === max) {
                  break loop;
                }
              }
            }
          } else { // search all fields
            for (var name in object) {
              var val = object[name];
              var id = new phantasus.Identifier([val, name]);
              if (!set.has(id) && regex.test(val)) {
                set.add(id);
                if (set.size() === max) {
                  break loop;
                }
              }
            }
          }

        }
      }
    }
    set.forEach(function (id) {
      var array = id.getArray();
      var field = array[1];
      var val = array[0];
      matches.push({
        value: field + ':' + val,
        label: '<span style="font-weight:300;">' + field + ':</span>'
        + '<span style="font-weight:900;">' + val + '</span>'
      });

    });
    if (field == null) {
      fields.forEach(function (field) {
        if (regex.test(field)) {
          matches.push({
            value: field + ':',
            label: '<span style="font-weight:300;">' + field
            + ':</span>',
            show: true
          });
        }
      });
    }
    cb(matches);
  };

};
// phantasus.DatasetUtil.toJSON = function(dataset) {
// var json = [];
// json.push('{');
// json.push('"name":"' + dataset.getName() + '", ');
// json.push('"v":['); // row major 2d array
// for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
// if (i > 0) {
// json.push(',\n');
// }
// json.push('[');
// for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
// if (j > 0) {
// json.push(',');
// }
// json.push(JSON.stringify(dataset.getValue(i, j)));
// }
// json.push(']');
// }
// json.push(']'); // end v
// var metadatatoJSON = function(model) {
// json.push('[');
// for (var i = 0, count = model.getMetadataCount(); i < count; i++) {
// var v = model.get(i);
// if (i > 0) {
// json.push(',\n');
// }
// json.push('{');
// json.push('"id":"' + v.getName() + '"');
// json.push(', "v":[');
// for (var j = 0, nitems = v.size(); j < nitems; j++) {
// if (j > 0) {
// json.push(',');
// }
// json.push(JSON.stringify(v.getValue(j)));
// }
// json.push(']'); // end v array
// json.push('}');
// }
// json.push(']');
// };
// json.push(', "cols":');
// metadatatoJSON(dataset.getColumnMetadata());
// json.push(', "rows":');
// metadatatoJSON(dataset.getRowMetadata());
// json.push('}'); // end json object
// return json.join('');
// };
phantasus.DatasetUtil.fill = function (dataset, value, seriesIndex) {
  seriesIndex = seriesIndex || 0;
  for (var i = 0, nrows = dataset.getRowCount(), ncols = dataset
    .getColumnCount(); i < nrows; i++) {
    for (var j = 0; j < ncols; j++) {
      dataset.setValue(i, j, value, seriesIndex);
    }
  }
};

/**
 * Add an additional series to a dataset from another dataset.
 * @param options.dataset The dataset to add a series to
 * @param options.newDataset The dataset that is used as the source for the overlay
 * @param options.rowAnnotationName dataset row annotation name to use for matching
 * @param options.columnAnnotationName dataset column annotation name to use for matching
 * @param options.newRowAnnotationName newDataset row annotation name to use for matching
 * @param options.newColumnAnnotationName newDataset column annotation name to use for matching
 *
 */
phantasus.DatasetUtil.overlay = function (options) {
  var dataset = options.dataset;
  var newDataset = options.newDataset;
  var current_dataset_row_annotation_name = options.rowAnnotationName;
  var current_dataset_column_annotation_name = options.columnAnnotationName;
  var new_dataset_row_annotation_name = options.newRowAnnotationName;
  var new_dataset_column_annotation_name = options.newColumnAnnotationName;

  var rowValueToIndexMap = phantasus.VectorUtil
    .createValueToIndexMap(dataset
      .getRowMetadata()
      .getByName(
        current_dataset_row_annotation_name));
  var columnValueToIndexMap = phantasus.VectorUtil
    .createValueToIndexMap(dataset
      .getColumnMetadata()
      .getByName(
        current_dataset_column_annotation_name));
  var seriesIndex = dataset
    .addSeries({
      name: newDataset
        .getName(),
      dataType: newDataset.getDataType(0)
    });

  var rowVector = newDataset
    .getRowMetadata()
    .getByName(
      new_dataset_row_annotation_name);
  var rowIndices = [];
  var newDatasetRowIndicesSubset = [];
  for (var i = 0, size = rowVector
    .size(); i < size; i++) {
    var index = rowValueToIndexMap
      .get(rowVector
        .getValue(i));
    if (index !== undefined) {
      rowIndices.push(index);
      newDatasetRowIndicesSubset
        .push(i);
    }
  }

  var columnVector = newDataset
    .getColumnMetadata()
    .getByName(
      new_dataset_column_annotation_name);
  var columnIndices = [];
  var newDatasetColumnIndicesSubset = [];
  for (var i = 0, size = columnVector
    .size(); i < size; i++) {
    var index = columnValueToIndexMap
      .get(columnVector
        .getValue(i));
    if (index !== undefined) {
      columnIndices.push(index);
      newDatasetColumnIndicesSubset
        .push(i);
    }
  }
  newDataset = new phantasus.SlicedDatasetView(
    newDataset,
    newDatasetRowIndicesSubset,
    newDatasetColumnIndicesSubset);
  for (var i = 0, nrows = newDataset
    .getRowCount(); i < nrows; i++) {
    for (var j = 0, ncols = newDataset
      .getColumnCount(); j < ncols; j++) {
      dataset.setValue(
        rowIndices[i],
        columnIndices[j],
        newDataset
          .getValue(
            i,
            j),
        seriesIndex);

    }
  }
};
/**
 * Joins datasets by appending rows.
 * @param datasets
 * @param field
 * @return {phantasus.AbstractDataset} The joined dataset.
 */
phantasus.DatasetUtil.join = function (datasets, field) {
  if (datasets.length === 0) {
    throw 'No datasets';
  }
  if (datasets.length === 1) {
    var name = datasets[0].getName();
    var sourceVector = datasets[0].getRowMetadata().add('Source');
    for (var i = 0, size = sourceVector.size(); i < size; i++) {
      sourceVector.setValue(i, name);
    }
    return datasets[0];
  }
  // take union of all ids
  var ids = new phantasus.Set();
  for (var i = 0; i < datasets.length; i++) {
    var idVector = datasets[i].getColumnMetadata().getByName(field);
    for (var j = 0, size = idVector.size(); j < size; j++) {
      ids.add(idVector.getValue(j));
    }
  }
  var dummyDataset = new phantasus.Dataset({
    rows: 0,
    columns: ids.size(),
    name: datasets[0].getName()
  });
  var dummyIdVector = dummyDataset.getColumnMetadata().add(field);
  var counter = 0;
  ids.forEach(function (id) {
    dummyIdVector.setValue(counter++, id);
  });

  var dataset = new phantasus.JoinedDataset(
    dummyDataset, datasets[0], field,
    field);
  for (var i = 1; i < datasets.length; i++) {
    dataset = new phantasus.JoinedDataset(dataset,
      datasets[i], field, field);
  }
  return dataset;
};
phantasus.DatasetUtil.shallowCopy = function (dataset) {
  // make a shallow copy of the dataset, metadata is immutable via the UI
  var rowMetadataModel = phantasus.MetadataUtil.shallowCopy(dataset
    .getRowMetadata());
  var columnMetadataModel = phantasus.MetadataUtil.shallowCopy(dataset
    .getColumnMetadata());
  dataset.getRowMetadata = function () {
    return rowMetadataModel;
  };
  dataset.getColumnMetadata = function () {
    return columnMetadataModel;
  };
  return dataset;
};

phantasus.DatasetUtil.copy = function (dataset) {
  var newDataset = new phantasus.Dataset({
    name: dataset.getName(),
    rows: dataset.getRowCount(),
    columns: dataset.getColumnCount(),
    dataType: dataset.getDataType(0)
  });
  for (var seriesIndex = 0,
         nseries = dataset.getSeriesCount(); seriesIndex < nseries; seriesIndex++) {
    if (seriesIndex > 0) {
      newDataset.addSeries({
        name: dataset.getName(seriesIndex),
        rows: dataset.getRowCount(),
        columns: dataset.getColumnCount(),
        dataType: dataset.getDataType(seriesIndex)
      });
    }
    for (var i = 0, nrows = dataset.getRowCount(), ncols = dataset
      .getColumnCount(); i < nrows; i++) {
      for (var j = 0; j < ncols; j++) {
        newDataset.setValue(i, j, dataset.getValue(i, j, seriesIndex),
          seriesIndex);
      }
    }
  }
  var rowMetadataModel = phantasus.MetadataUtil.shallowCopy(dataset
    .getRowMetadata());
  var columnMetadataModel = phantasus.MetadataUtil.shallowCopy(dataset
    .getColumnMetadata());
  newDataset.getRowMetadata = function () {
    return rowMetadataModel;
  };
  newDataset.getColumnMetadata = function () {
    return columnMetadataModel;
  };
  if (dataset.getESSession()) {
    newDataset.setESSession(dataset.getESSession());
  }
  return newDataset;
};
phantasus.DatasetUtil.toString = function (dataset, value, seriesIndex) {
  seriesIndex = seriesIndex || 0;
  var s = [];
  for (var i = 0, nrows = dataset.getRowCount(), ncols = dataset
    .getColumnCount(); i < nrows; i++) {
    for (var j = 0; j < ncols; j++) {
      if (j > 0) {
        s.push(', ');
      }
      s.push(phantasus.Util.nf(dataset.getValue(i, j, seriesIndex)));
    }
    s.push('\n');
  }
  return s.join('');
};
phantasus.DatasetUtil.getNonEmptyRows = function (dataset) {
  var rowsToKeep = [];
  for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
    var keep = false;
    for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
      var value = dataset.getValue(i, j);
      if (!isNaN(value)) {
        keep = true;
        break;
      }
    }
    if (keep) {
      rowsToKeep.push(i);
    }
  }
  return rowsToKeep;
};
phantasus.DatasetUtil.getContentArray = function (dataset) {
  var array = [];
  var nr = dataset.rows;
  var nc = dataset.columns;
  //// console.log("getContentArray ::", "dataset:", dataset, "rows:", nr, "columns:", nc);

  for (var i = 0; i < nc; i++) {
    for (var j = 0; j < nr; j++) {
      array.push(dataset.getValue(j, i));
    }
  }
  //// console.log("getContentArray ::", array);
  return array;
};
phantasus.DatasetUtil.getMetadataArray = function (dataset) {
  var pDataArray = [];
  var labelDescription = [];
  //// console.log("phantasus.DatasetUtil.getMetadataArray ::", dataset);
  var columnMeta = dataset.getColumnMetadata();
  var features = columnMeta.getMetadataCount();
  var participants = dataset.getColumnCount();


  for (var j = 0; j < features; j++) {
    var vecJ = columnMeta.get(j);
    for (var l = 0; l < participants; l++) {
      pDataArray.push({
        strval: vecJ.getValue(l) ? vecJ.getValue(l).toString() : "",
        isNA: false
      });
    }
    labelDescription.push({
      strval: vecJ.getName(),
      isNA: false
    });

  }

  var rowMeta = dataset.getRowMetadata();
  var fDataArray = [];
  var varLabels = [];
  for (var j = 0; j < rowMeta.getMetadataCount(); j++) {
    var vecJ = rowMeta.get(j);
    for (var l = 0; l < dataset.getRowCount(); l++) {
      fDataArray.push({
        strval: vecJ.getValue(l) ? vecJ.getValue(l).toString() : "",
        isNA: false
      });
    }
    varLabels.push({
      strval: vecJ.getName(),
      isNA: false
    });
  }

  return {
    pdata: pDataArray,
    varLabels: labelDescription,
    fdata: fDataArray,
    fvarLabels: varLabels
  };
};

phantasus.DatasetUtil.toESSessionPromise = function (options) {
  var dataset = options.dataset ? options.dataset : options;

  //// console.log("ENTERED TO_ESSESSION_PROMISE", dataset, options);
  //var copiedDataset = phantasus.DatasetUtil.copy(dataset);
  //// console.log("EsSessionPromise ::", "after copying", dataset);
  while (dataset.dataset) {
    dataset = dataset.dataset;
  }
  // console.log(dataset);
  dataset.setESSession(new Promise(function (resolve, reject) {
    //// console.log("phantasus.DatasetUtil.toESSessionPromise ::", dataset, dataset instanceof phantasus.Dataset, dataset instanceof phantasus.SlicedDatasetView);
    /*		if (dataset.dataset) {
     //// console.log("phantasus.DatasetUtil.toESSessionPromise ::", "dataset in instanceof phantasus.SlicedDatasetView", "go deeper");
     phantasus.DatasetUtil.toESSessionPromise(dataset.dataset);
     }*/
    //// console.log("before going further", options);
    if (options.isGEO || options.preloaded) {
      //// console.log("toESSession::", "resolving as geo dataset");
      resolve(dataset.getESSession());
      return;
    }

    var array = phantasus.DatasetUtil.getContentArray(dataset);
    var meta = phantasus.DatasetUtil.getMetadataArray(dataset);

    // console.log(array, array.length);
    var messageJSON = {
      rclass: "LIST",
      rexpValue: [{
        rclass: "REAL",
        realValue: array,
        attrName: "dim",
        attrValue: {
          rclass: "INTEGER",
          intValue: [dataset.getRowCount(), dataset.getColumnCount()]
        }
      }, {
        rclass: "STRING",
        stringValue: meta.pdata,
        attrName: "dim",
        attrValue: {
          rclass: "INTEGER",
          intValue: [dataset.getColumnCount(), meta.varLabels.length]
        }
      }, {
        rclass: "STRING",
        stringValue: meta.varLabels
      }, {
        rclass: "STRING",
        stringValue: meta.fdata,
        attrName: "dim",
        attrValue: {
          rclass: "INTEGER",
          intValue: [dataset.getRowCount(), meta.fvarLabels.length]
        }
      }, {
        rclass: "STRING",
        stringValue: meta.fvarLabels
      }],
      attrName: "names",
      attrValue: {
        rclass: "STRING",
        stringValue: [{
          strval: "data",
          isNA: false
        }, {
          strval: "pData",
          isNA: false
        }, {
          strval: "varLabels",
          isNA: false
        }, {
          strval: "fData",
          isNA: false
        }, {
          strval: "fvarLabels",
          isNA: false
        }]
      }
    };
    var ProtoBuf = dcodeIO.ProtoBuf;
    ProtoBuf.protoFromFile('./message.proto', function (error, success) {
      if (error) {
        alert(error);
        // console.log("ExpressionSetCreation :: ", "ProtoBuilder failed", error);
        return;
      }
      //// console.log("phantasus.DatasetUtil.toESSessionPromise ::", "protobuilder error", error);
      //// console.log("phantasus.DatasetUtil.toESSessionPromise ::", "protobuilder success", success);
      var builder = success,
        rexp = builder.build('rexp'),
        REXP = rexp.REXP;

      var proto = new REXP(messageJSON);
      var req = ocpu.call('createES', proto, function (session) {
        //// console.log("phantasus.DatasetUtil.toESSessionPromise ::", "from successful request", session);
        dataset.setESVariable('es');
        resolve(session);
      }, true);

      req.fail(function () {
        reject(req.responseText);
      });
    });
  }));
  /*var blob = new Blob([new Uint8Array((new REXP(messageJSON)).toArrayBuffer())], {type: "application/octet-stream"});
   saveAs(blob, "test1.bin");*/
};

/**
 * Default implementation of a dataset.
 *
 * @extends {phantasus.AbstractDataset}
 * @param options.rows {number} Number of rows
 * @param options.columns {number} Number of columns
 * @param options.name {string} Dataset name
 * @param options.dataType {string=} Data type that 1st series holds.
 * @param options.esSession {Promise} openCPU session, which contains ExpressionSet version of the dataset
 * @constructor
 */
phantasus.Dataset = function (options) {
  phantasus.AbstractDataset.call(this, options.rows,
    options.columns);

  if (options.dataType == null) {
    options.dataType = 'Float32';
  }

  if (options.esSession) {
    this.esSession = options.esSession;
  }
  if (options.esVariable) {
    this.esVariable = options.esVariable;
  }
  this.seriesNames.push(options.name);
  this.seriesArrays.push(options.array ? options.array : phantasus.Dataset
    .createArray(options));
  this.seriesDataTypes.push(options.dataType);
  //// console.log(this);
};
/**
 *
 * @param dataset
 * @param options.rowFields
 * @param options.columnFields
 * @param options.seriesIndices
 * @return JSON representation of a dataset
 */
phantasus.Dataset.toJSON = function (dataset, options) {
  options = options || {};
  var seriesArrays = [];
  var seriesDataTypes = [];
  var seriesNames = [];
  var seriesIndices = options.seriesIndices;
  if (seriesIndices == null) {
    seriesIndices = phantasus.Util.sequ32(dataset.getSeriesCount());
  }
  for (var series = 0; series < seriesIndices.length; series++) {
    var seriesIndex = seriesIndices[series];
    seriesNames.push(dataset.getName(seriesIndex));
    seriesDataTypes.push(dataset.getDataType(seriesIndex));
    var data = [];
    seriesArrays.push(data);
    for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
      var row = [];
      data.push(row);
      for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
        row[j] = dataset.getValue(i, j, seriesIndex);
      }
    }
  }
  var vectorToJSON = function (vector) {
    var array = [];
    for (var i = 0, size = vector.size(); i < size; i++) {
      array[i] = vector.getValue(i);
    }
    var properties = new phantasus.Map();
    vector.getProperties().forEach(function (value, key) {
      if (phantasus.VectorKeys.JSON_WHITELIST.has(key)) {
        properties.set(key, value);
      }
    });
    return {
      properties: properties,
      name: vector.getName(),
      array: array
    };
  };
  var metadatatoJSON = function (metadata, fields) {
    var vectors = [];
    var filter;
    if (fields) {
      filter = new phantasus.Set();
      fields.forEach(function (field) {
        filter.add(field);
      });
    }
    for (var i = 0, count = metadata.getMetadataCount(); i < count; i++) {
      var v = metadata.get(i);
      if (!v.getProperties().has(phantasus.VectorKeys.IS_INDEX)) {
        if (filter) {
          if (filter.has(v.getName())) {
            vectors.push(vectorToJSON(v));
          }
        } else {
          vectors.push(vectorToJSON(v));
        }
      }
    }
    return vectors;
  };
  return {
    rows: dataset.getRowCount(),
    columns: dataset.getColumnCount(),
    seriesArrays: seriesArrays,
    seriesDataTypes: seriesDataTypes,
    seriesNames: seriesNames,
    rowMetadataModel: {
      vectors: metadatatoJSON(dataset.getRowMetadata(),
        options.rowFields)
    },
    columnMetadataModel: {
      vectors: metadatatoJSON(dataset.getColumnMetadata(),
        options.columnFields)
    }
  };
};
phantasus.Dataset.fromJSON = function (options) {
  // Object {seriesNames:
  // Array[1], seriesArrays:
  // Array[1], rows:
  // 6238, columns: 7251,
  // rowMetadataModel: Object…}
  // columnMetadataModel: Object
  // itemCount: 7251
  // vectors: Array[3]
  // array: Array[7251]
  // n: 7251
  // name: "pert_id"
  // properties: Object
  // columns: 7251
  // rowMetadataModel: Object
  // rows: 6238
  // seriesArrays: Array[1]
  // seriesNames: Array[1]
  // var array = phantasus.Dataset.createArray(options);
  // for (var i = 0; i < options.rows; i++) {
  // var row = array[i];
  // var jsonRow = options.array[i];
  // for (var j = 0; j < options.columns; j++) {
  // row[j] = jsonRow[j];
  // }
  // }

  if (options.seriesMappings) {
    for (var seriesIndex = 0; seriesIndex < options.seriesMappings.length; seriesIndex++) {
      // map ordinal values
      if (options.seriesMappings[seriesIndex]) {

        var map = options.seriesMappings[seriesIndex]; // e.g. foo:1, bar:3
        var valueMap = new phantasus.Map();
        for (var key in map) {
          var value = map[key];
          valueMap.set(value, phantasus.Util.wrapNumber(value, key));
        }

        var array = options.seriesArrays[seriesIndex];
        for (var i = 0; i < options.rows; i++) {
          for (var j = 0; j < options.columns; j++) {
            var value = array[i][j];
            array[i][j] = valueMap.get(value);
          }
        }
        options.seriesDataTypes[seriesIndex] = 'Number';
      }
    }
  }

  for (var seriesIndex = 0; seriesIndex < options.seriesArrays.length; seriesIndex++) {
    var array = options.seriesArrays[seriesIndex];
    for (var i = 0; i < options.rows; i++) {
      for (var j = 0; j < options.columns; j++) {
        var value = array[i][j];
        if (value == null) {
          array[i][j] = NaN;
        }
      }
    }
  }
  var dataset = new phantasus.Dataset({
    name: options.seriesNames[0],
    dataType: options.seriesDataTypes[0],
    array: options.seriesArrays[0],
    rows: options.rows,
    columns: options.columns
  });

  if (options.rowMetadataModel) {
    options.rowMetadataModel.vectors.forEach(function (v) {
      var vector = new phantasus.Vector(v.name, dataset.getRowCount());
      vector.array = v.array;
      vector.properties = phantasus.Map.fromJSON(v.properties);
      dataset.rowMetadataModel.vectors.push(vector);
    });
  }
  if (options.columnMetadataModel) {
    options.columnMetadataModel.vectors.forEach(function (v) {
      var vector = new phantasus.Vector(v.name, dataset.getColumnCount());
      vector.array = v.array;
      vector.properties = phantasus.Map.fromJSON(v.properties);
      dataset.columnMetadataModel.vectors.push(vector);

    });
  }
  for (var i = 1; i < options.seriesArrays.length; i++) {
    dataset.addSeries({
      name: options.seriesNames[i],
      dataType: options.seriesDataTypes[i],
      array: options.seriesArrays[i]
    });
  }
  return dataset;
};
phantasus.Dataset.createArray = function (options) {
  var array = [];
  if (options.dataType == null || options.dataType === 'Float32') {
    for (var i = 0; i < options.rows; i++) {
      array.push(new Float32Array(options.columns));
    }
  } else if (options.dataType === 'Int8') {
    for (var i = 0; i < options.rows; i++) {
      array.push(new Int8Array(options.columns));
    }
  } else if (options.dataType === 'Int16') {
    for (var i = 0; i < options.rows; i++) {
      array.push(new Int16Array(options.columns));
    }
  } else { // [object, number, Number] array of arrays
    for (var i = 0; i < options.rows; i++) {
      array.push([]);
    }
  }
  return array;
};
phantasus.Dataset.prototype = {
  getValue: function (i, j, seriesIndex) {
    seriesIndex = seriesIndex || 0;
    return this.seriesArrays[seriesIndex][i][j];
  },
  toString: function () {
    return this.getName();
  },
  setValue: function (i, j, value, seriesIndex) {
    seriesIndex = seriesIndex || 0;
    this.seriesArrays[seriesIndex][i][j] = value;
  },
  addSeries: function (options) {
    options = $.extend({}, {
      rows: this.getRowCount(),
      columns: this.getColumnCount(),
      dataType: 'Float32'
    }, options);
    this.seriesDataTypes.push(options.dataType);
    this.seriesNames.push(options.name);
    this.seriesArrays.push(options.array != null ? options.array
      : phantasus.Dataset.createArray(options));
    return this.seriesNames.length - 1;
  },
  setESSession: function (session) {
    //// console.log("phantasus.Dataset.prototype.setESSession ::", this, session);
    this.esSession = session;
  },
  getESSession: function () {
    //// console.log("phantasus.Dataset.prototype.getESSession ::", this);
    return this.esSession;
  },

  getESVariable: function () {
    return this.esVariable;
  },

  setESVariable: function(variable) {
    this.esVariable = variable;
  }

};
phantasus.Util.extend(phantasus.Dataset, phantasus.AbstractDataset);

phantasus.ElementSelectionModel = function (project) {
  this.viewIndices = new phantasus.Set();
  this.project = project;
};
phantasus.ElementSelectionModel.prototype = {
  click: function (rowIndex, columnIndex, add) {
    var id = new phantasus.Identifier([rowIndex, columnIndex]);
    var isSelected = this.viewIndices.has(id);
    if (add) {
      isSelected ? this.viewIndices.remove(id) : this.viewIndices.add(id);
    } else {
      this.viewIndices.clear();
      if (!isSelected) {
        this.viewIndices.add(id);
      }
    }
    this.trigger('selectionChanged');
  },
  getProject: function () {
    return this.project;
  },
  setViewIndices: function (indices) {
    this.viewIndices = indices;
    this.trigger('selectionChanged');
  },
  clear: function () {
    this.viewIndices = new phantasus.Set();
  },
  /**
   *
   * @returns {phantasus.Set}
   */
  getViewIndices: function () {
    return this.viewIndices;
  },
  count: function () {
    return this.viewIndices.size();
  },
  toModelIndices: function () {
    var project = this.project;
    var modelIndices = [];
    this.viewIndices.forEach(function (id) {
      modelIndices.push(project
        .convertViewRowIndexToModel(id.getArray()[0]), project
        .convertViewColumnIndexToModel(id.getArray()[1]));
    });
    return modelIndices;
  },
  save: function () {
    this.modelIndices = this.toModelIndices();
  },
  restore: function () {
    var project = this.project;
    this.viewIndices = new phantasus.Set();
    for (var i = 0, length = this.modelIndices.length; i < length; i++) {
      var rowIndex = project
        .convertModelRowIndexToView(this.modelIndices[i][0]);
      var columnIndex = project
        .convertModelColumnIndexToView(this.modelIndices[i][1]);
      if (rowIndex !== -1 && columnIndex !== -1) {
        this.viewIndices.add(new phantasus.Identifier([rowIndex,
          columnIndex]));
      }
    }
  }
};
phantasus.Util.extend(phantasus.ElementSelectionModel, phantasus.Events);

phantasus.CombinedFilter = function (isAndFilter) {
  this.filters = [];
  this.isAndFilter = isAndFilter;
  this.enabledFilters = [];
  this.name = 'combined filter';
};

phantasus.CombinedFilter.prototype = {
  shallowClone: function () {
    var f = new phantasus.CombinedFilter(this.isAndFilter);
    f.filters = this.filters.slice(0);
    return f;
  },
  isColumns: function () {
    return this.filters[0].isColumns();
  },
  toString: function () {
    return this.name;
  },
  setAnd: function (isAndFilter, notify) {
    this.isAndFilter = isAndFilter;
    if (notify) {
      this.trigger('and', {});
    }
  },
  isAnd: function () {
    return this.isAndFilter;
  },
  equals: function (f) {
    if (!(f instanceof phantasus.CombinedFilter)) {
      return false;
    }
    if (this.isAndFilter !== f.isAndFilter) {
      return false;
    }
    if (this.filters.length !== f.filters.length) {
      return false;
    }
    for (var i = 0, length = this.filters.length; i < length; i++) {
      if (!this.filters[i].equals(f.filters[i])) {
        return false;
      }
    }
    return true;
  },
  add: function (filter, notify) {
    this.filters.push(filter);
    if (notify) {
      this.trigger('add', {
        filter: filter,
      });
    }
  },
  getFilters: function () {
    return this.filters;
  },
  get: function (index) {
    return this.filters[index];
  },
  indexOf: function (name, type) {
    for (var i = 0, length = this.filters.length; i < length; i++) {
      if (this.filters[i].toString() === name
        && (type == null ? true : this.filters[i] instanceof type)) {
        return i;
      }
    }
    return -1;
  },
  remove: function (index, notify) {
    this.filters.splice(index, 1);
    if (notify) {
      this.trigger('remove', {
        index: index,
      });
    }
  },
  set: function (index, filter) {
    this.filters[index] = filter;
  },
  insert: function (index, filter) {
    this.filters.splice(index, 0, filter);
  },
  clear: function () {
    this.filters = [];
  },
  init: function (dataset) {
    for (var i = 0, nfilters = this.filters.length; i < nfilters; i++) {
      if (this.filters[i].isColumns()) { // all filters operate on rows
        this.filters[i].init(new phantasus.TransposedDatasetView(dataset));
      } else {
        this.filters[i].init(dataset);
      }

    }
    this.enabledFilters = this.filters.filter(function (filter) {
      return filter.isEnabled();
    });
  },
  accept: function (index) {
    var filters = this.enabledFilters;
    if (this.isAndFilter) {
      for (var i = 0, nfilters = filters.length; i < nfilters; i++) {
        if (filters[i].accept(index) === false) {
          return false;
        }
      }
      return true;
    } else {
      for (var i = 0, nfilters = filters.length; i < nfilters; i++) {
        if (filters[i].accept(index)) {
          return true;
        }
      }
      return false;
    }
  },
  isEnabled: function () {
    return this.enabledFilters.length > 0;
  },
};
phantasus.Util.extend(phantasus.CombinedFilter, phantasus.Events);
/**
 * @param acceptIndicesSet
 *            a phantasus.Set that contains the model indices in the dataset to
 *            retain.
 */
phantasus.IndexFilter = function (acceptIndicesSet, name, isColumns) {
  this.acceptIndicesSet = acceptIndicesSet;
  this.name = name;
  this.columns = isColumns;
};
phantasus.IndexFilter.prototype = {
  enabled: true,
  isColumns: function () {
    return this.columns;
  },
  isEnabled: function () {
    return this.enabled;
  },
  setAcceptIndicesSet: function (acceptIndicesSet) {
    this.acceptIndicesSet = acceptIndicesSet;
  },
  setEnabled: function (enabled) {
    this.enabled = enabled;
  },
  equals: function (filter) {
    return filter instanceof phantasus.IndexFilter
      && this.acceptIndicesSet.equals(filter.acceptIndicesSet);
  },
  init: function (dataset) {
  },
  toString: function () {
    return this.name;
  },
  /**
   *
   * @param index
   *            The model index in the dataset
   * @returns {Boolean} true if index passes filter
   */
  accept: function (index) {
    return this.acceptIndicesSet.has(index);
  },
};
phantasus.VectorFilter = function (set, maxSetSize, name, isColumns) {
  this.set = set;
  this.name = name;
  this.maxSetSize = maxSetSize;
  this.columns = isColumns;
};

phantasus.VectorFilter.prototype = {
  enabled: true,
  isColumns: function () {
    return this.columns;
  },
  isEnabled: function () {
    return this.enabled && this.set.size() > 0
      && this.set.size() !== this.maxSetSize && this.vector != null;
  },
  setEnabled: function (enabled) {
    this.enabled = enabled;
  },
  equals: function (filter) {
    return filter instanceof phantasus.VectorFilter
      && this.name === filter.name;
  },
  init: function (dataset) {
    this.vector = dataset.getRowMetadata().getByName(this.name);
  },
  toString: function () {
    return this.name;
  },
  /**
   *
   * @param index
   *            The model index in the dataset
   * @returns {Boolean} true if index passes filter
   */
  accept: function (index) {
    return this.set.has(this.vector.getValue(index));
  },
};

phantasus.NotNullFilter = function (name, isColumns) {
  this.name = name;
  this.columns = isColumns;
};
phantasus.NotNullFilter.prototype = {
  enabled: true,
  isColumns: function () {
    return this.columns;
  },
  isEnabled: function () {
    return this.enabled && this.vector != null;
  },
  setEnabled: function (enabled) {
    this.enabled = enabled;
  },
  equals: function (filter) {
    return filter instanceof phantasus.NotNullFilter
      && this.name === filter.name;
  },
  init: function (dataset) {
    this.vector = dataset.getRowMetadata().getByName(this.name);
  },
  toString: function () {
    return this.name;
  },
  /**
   *
   * @param index
   *            The model index in the dataset
   * @returns {Boolean} true if index passes filter
   */
  accept: function (index) {
    return this.vector.getValue(index) != null;
  },
};

phantasus.RangeFilter = function (min, max, name, isColumns) {
  this.min = min;
  this.max = max;
  this.name = name;
  this.columns = isColumns;
};

phantasus.RangeFilter.prototype = {
  enabled: true,
  isColumns: function () {
    return this.columns;
  },
  isEnabled: function () {
    return this.enabled && (!isNaN(this.min) || !isNaN(this.max))
      && this.vector;
  },
  setEnabled: function (enabled) {
    this.enabled = enabled;
  },
  setMin: function (value) {
    this.min = isNaN(value) ? -Number.MAX_VALUE : value;
  },
  setMax: function (value) {
    this.max = isNaN(value) ? Number.MAX_VALUE : value;
  },
  equals: function (filter) {
    return filter instanceof phantasus.RangeFilter
      && this.name === filter.name;
  },
  init: function (dataset) {
    this.vector = dataset.getRowMetadata().getByName(this.name);

  },
  toString: function () {
    return this.name;
  },
  /**
   *
   * @param index
   *            The model index in the dataset
   * @returns {Boolean} true if index passes filter
   */
  accept: function (index) {
    var value = this.vector.getValue(index);
    return value >= this.min && value <= this.max;
  },
};

phantasus.TopNFilter = function (n, direction, name, isColumns) {
  this.n = n;
  this.direction = direction;
  this.name = name;
  this.columns = isColumns;
};

phantasus.TopNFilter.TOP = 0;
phantasus.TopNFilter.BOTTOM = 1;
phantasus.TopNFilter.TOP_BOTTOM = 2;
phantasus.TopNFilter.prototype = {
  enabled: true,
  isColumns: function () {
    return this.columns;
  },
  isEnabled: function () {
    return this.enabled && this.n > 0 && this.vector;
  },
  setEnabled: function (enabled) {
    this.enabled = enabled;
  },
  setN: function (value) {
    this.n = value;
  },
  /**
   *
   * @param direction
   *            one of '
   */
  setDirection: function (direction) {
    this.direction = direction;
  },
  equals: function (filter) {
    return filter instanceof phantasus.TopNFilter
      && this.name === filter.name && this.n === filter.n
      && this.direction === filter.direction;
  },

  init: function (dataset) {
    if (!this.vector ||
      this.vector !== dataset.getRowMetadata().getByName(this.name)) {
      var vector = dataset.getRowMetadata().getByName(this.name);
      if (vector == null) {
        vector = {
          getValue: function () {
          },
          size: function () {
            return 0;
          },
        };
      }
      this.vector = vector;
      var set = new phantasus.Set();
      for (var i = 0, size = vector.size(); i < size; i++) {
        var value = vector.getValue(i);
        if (!isNaN(value)) {
          set.add(value);
        }
      }
      var values = set.values();
      // ascending order
      values.sort(function (a, b) {
        return (a === b ? 0 : (a < b ? -1 : 1));
      });
      this.sortedValues = values;
    }
    var topAndBottomIndices = [
      (this.sortedValues.length - this.n),
      (this.n - 1)];

    for (var i = 0; i < topAndBottomIndices.length; i++) {
      topAndBottomIndices[i] = Math.max(0, topAndBottomIndices[i]);
      topAndBottomIndices[i] = Math.min(this.sortedValues.length - 1,
        topAndBottomIndices[i]);
    }

    var topAndBottomValues = [
      this.sortedValues[topAndBottomIndices[0]],
      this.sortedValues[topAndBottomIndices[1]]];

    if (this.direction === phantasus.TopNFilter.TOP) {
      this.f = function (val) {
        return isNaN(val) ? false : val >= topAndBottomValues[0];
      };
    } else if (this.direction === phantasus.TopNFilter.BOTTOM) {
      this.f = function (val) {
        return isNaN(val) ? false : val <= topAndBottomValues[1];
      };
    } else {
      this.f = function (val) {
        return isNaN(val) ? false
          : (val >= topAndBottomValues[0] || val <= topAndBottomValues[1]);
      };
    }

  },
  /**
   *
   * @param index
   *            The model index in the dataset
   * @returns {Boolean} true if index passes filter
   */
  accept: function (index) {
    return this.f(this.vector.getValue(index));
  },
  toString: function () {
    return this.name;
  },
};

phantasus.AlwaysTrueFilter = function () {

};

phantasus.AlwaysTrueFilter.prototype = {
  isEnabled: function () {
    return false;
  },
  setEnabled: function (enabled) {

  },
  equals: function (filter) {
    return filter instanceof phantasus.AlwaysTrueFilter;

  },
  init: function (dataset) {

  },
  toString: function () {
    return 'AlwaysTrue';
  },
  /**
   *
   * @param index
   *            The model index in the dataset
   * @returns {Boolean} true if index passes filter
   */
  accept: function (index) {
    return true;
  },
};

phantasus.CombinedFilter.fromJSON = function (combinedFilter, json) {
  combinedFilter.setAnd(json.isAnd);
  json.filters.forEach(function (filter) {
    var name = filter.name != null ? filter.name : filter.field;
    if (filter.type === 'set') {
      var set = new phantasus.Set();
      filter.values.forEach(function (value) {
        set.add(value);
      });
      combinedFilter.add(new phantasus.VectorFilter(
        set,
        filter.maxSetSize,
        name,
        filter.isColumns
      ));
    } else if (filter.type === 'range') {
      combinedFilter.add(new phantasus.RangeFilter(
        filter.min,
        filter.max,
        name,
        filter.isColumns
      ));
    } else if (filter.type === 'top') {
      if (_.isString(filter.direction)) {
        if (filter.direction === 'top') {
          filter.direction = phantasus.TopNFilter.TOP;
        } else if (filter.direction === 'bottom') {
          filter.direction = phantasus.TopNFilter.BOTTOM;
        } else if (filter.direction === 'topAndBottom') {
          filter.direction = phantasus.TopNFilter.TOP_BOTTOM;
        }
      }
      combinedFilter.add(new phantasus.TopNFilter(
        filter.n,
        filter.direction,
        name,
        filter.isColumns
      ));
    } else if (filter.type === 'index') {
      var set = new phantasus.Set();
      filter.indices.forEach(function (value) {
        set.add(value);
      });
      combinedFilter.add(new phantasus.IndexFilter(
        set,
        name,
        filter.isColumns
      ));
    } else {
      // console.log('Unknown filter type');
    }
  });
};

phantasus.CombinedFilter.toJSON = function (filter) {
  var json = {
    isAnd: filter.isAnd(),
    filters: [],
  };
  filter.getFilters().forEach(function (filter) {
    if (filter.isEnabled()) {
      if (filter instanceof phantasus.VectorFilter) {
        // phantasus.VectorFilter = function (set, maxSetSize, name, isColumns)
        json.filters.push({
          name: filter.name,
          isColumns: filter.isColumns(),
          values: filter.set.values(),
          maxSetSize: filter.maxSetSize,
          type: 'set',
        });
      } else if (filter instanceof phantasus.RangeFilter) {
        // phantasus.RangeFilter = function (min, max, name, isColumns)
        json.filters.push({
          name: filter.name,
          isColumns: filter.isColumns(),
          min: filter.min,
          max: filter.max,
          type: 'range',
        });
      } else if (filter instanceof phantasus.TopNFilter) {
        // phantasus.TopNFilter = function (n, direction, name, isColumns)

        json.filters.push({
          name: filter.name,
          isColumns: filter.isColumns(),
          n: filter.n,
          direction: filter.direction,
          type: 'top',
        });
      } else if (filter instanceof phantasus.IndexFilter) {
        // phantasus.IndexFilter = function (acceptIndicesSet, name, isColumns
        json.filters.push({
          name: filter.name,
          isColumns: filter.isColumns(),
          indices: filter.acceptIndicesSet.values(),
          type: 'index',
        });
      }
    }
  });
  return json;
};

phantasus.IndexMapper = function (project, isRows) {
  this.project = project;
  this.isRows = isRows;
  this.sortKeys = [];
  /**
   * {phantasus.Map} Maps from model index to view index. Note that not all
   * model indices are contained in the map because they might have been
   * filtered from the view.
   */
  this.modelToView = null;
  /** {Array} filtered model indices */
  this.filteredModelIndices = null;
  /** {Array} sorted and filtered model indices */
  this.filteredSortedModelIndices = null;
  this.filter = new phantasus.CombinedFilter(true);
  this._filter();
  this._sort();
};

phantasus.IndexMapper.prototype = {
  convertModelIndexToView: function (modelIndex) {
    var index = this.modelToView.get(modelIndex);
    return index !== undefined ? index : -1;
  },
  convertViewIndexToModel: function (viewIndex) {
    return (viewIndex < this.filteredSortedModelIndices.length
    && viewIndex >= 0 ? this.filteredSortedModelIndices[viewIndex]
      : -1);
  },
  convertToView: function () {
    return this.filteredSortedModelIndices;
  },
  setFilter: function (filter) {
    this.filter = filter;
    this._filter();
    this._sort();
  },
  _filter: function () {
    var filter = this.filter;
    var dataset = this.project.getFullDataset();
    var count = this.isRows ? dataset.getRowCount() : dataset.getColumnCount();
    var filteredModelIndices;
    if (filter != null) {
      filter.init(dataset); // filter needs to transpose if columns
      if (filter.isEnabled()) {
        filteredModelIndices = [];

        for (var i = 0; i < count; i++) {
          if (filter.accept(i)) {
            filteredModelIndices.push(i);
          }
        }
      }
    }

    this.filteredModelIndices = filteredModelIndices != null ? filteredModelIndices
      : phantasus.Util.seq(count);
  },
  _sort: function () {
    var sortKeys = this.sortKeys;
    if (sortKeys.length > 0) {
      var dataset = this.project.getFullDataset();

      var nkeys = sortKeys.length;
      for (var i = 0; i < nkeys; i++) {
        sortKeys[i].init(sortKeys[i].isColumns() ? new phantasus.TransposedDatasetView(dataset) : dataset, this.filteredSortedModelIndices);
      }
      this.filteredSortedModelIndices = this.filteredModelIndices
        .slice(0);
      this.filteredSortedModelIndices.sort(function (a, b) {
        for (var i = 0; i < nkeys; i++) {
          var key = sortKeys[i];
          var comparator = key.getComparator();
          var val1 = key.getValue(a);
          var val2 = key.getValue(b);
          var c = comparator(val1, val2);
          if (c !== 0) {
            return c;
          }
        }
        return 0;
      });
    } else {
      this.filteredSortedModelIndices = this.filteredModelIndices;
    }

    var modelToView = new phantasus.Map();
    for (var i = 0, length = this.filteredSortedModelIndices.length; i < length; i++) {
      modelToView.set(this.filteredSortedModelIndices[i], i);
    }
    this.modelToView = modelToView;
  },
  getFilter: function () {
    return this.filter;
  },
  getViewCount: function () {
    if (this.project.getFullDataset() == null) {
      return 0;
    }
    return this.filteredSortedModelIndices.length;
  },
  setSelectedModelIndices: function (selectedModelIndices) {
    this.selectionModel.setSelectedModelIndices(selectedModelIndices);
  },
  setSortKeys: function (sortKeys) {
    if (sortKeys == null) {
      sortKeys = [];
    }
    this.sortKeys = sortKeys;
    this._sort();
  }
};

/**
 * Adds rows in dataset2 to dataset1
 */
phantasus.JoinedDataset = function (dataset1, dataset2, dataset1Field,
                                   dataset2Field, sourceFieldName) {
  sourceFieldName = sourceFieldName || 'Source';
  this.dataset1Field = dataset1Field;
  this.dataset2Field = dataset2Field;
  if (dataset1 == null) {
    throw 'dataset1 is null';
  }
  if (dataset2 == null) {
    throw 'dataset2 is null';
  }
  if (dataset1Field) { // reorder dataset 2 to match dataset 1
    var v1 = dataset1.getColumnMetadata().getByName(dataset1Field);
    var dataset2ValueToIndex = phantasus.VectorUtil
      .createValueToIndexMap(dataset2.getColumnMetadata().getByName(
        dataset2Field));
    var dataset2ColumnIndices = [];
    for (var i = 0; i < v1.size(); i++) {
      dataset2ColumnIndices[i] = dataset2ValueToIndex.get(v1.getValue(i));
      // undefined indices are handles in SlicedDatasetWithNulls
    }
    dataset2 = new phantasus.SlicedDatasetWithNulls(dataset2,
      dataset2ColumnIndices, dataset1.getColumnCount(), dataset1
        .getColumnMetadata());
  }

  if (!dataset1.getRowMetadata().getByName(sourceFieldName)) {
    var sourceVector = dataset1.getRowMetadata().add(sourceFieldName);
    var name = dataset1.getName();
    for (var i = 0, nrows = sourceVector.size(); i < nrows; i++) {
      sourceVector.setValue(i, name);
    }
  }
  if (!dataset2.getRowMetadata().getByName(sourceFieldName)) {
    var sourceVector = dataset2.getRowMetadata().add(sourceFieldName);
    var name = dataset2.getName();
    for (var i = 0, nrows = sourceVector.size(); i < nrows; i++) {
      sourceVector.setValue(i, name);
    }

  }

  // make sure dataset1 and dataset2 have the same row metadata fields in the
  // same order
  for (var i = 0, count = dataset1.getRowMetadata().getMetadataCount(); i < count; i++) {
    var name = dataset1.getRowMetadata().get(i).getName();
    if (dataset2.getRowMetadata().getByName(name) == null) {
      dataset2.getRowMetadata().add(name);
    }
  }
  for (var i = 0, count = dataset2.getRowMetadata().getMetadataCount(); i < count; i++) {
    var name = dataset2.getRowMetadata().get(i).getName();
    if (dataset1.getRowMetadata().getByName(name) == null) {
      dataset1.getRowMetadata().add(name);
    }
  }

  // put dataset2 row metadata names in same order as dataset1
  var dataset2RowMetadataOrder = [];
  var metadataInDifferentOrder = false;
  for (var i = 0, count = dataset1.getRowMetadata().getMetadataCount(); i < count; i++) {
    var name = dataset1.getRowMetadata().get(i).getName();
    var index = phantasus.MetadataUtil.indexOf(dataset2.getRowMetadata(),
      name);
    dataset2RowMetadataOrder.push(index);
    if (index !== i) {
      metadataInDifferentOrder = true;
    }
  }
  this.dataset1 = dataset1;
  this.dataset2 = dataset2;
  // TODO put series in same order
  var maxSeriesCount = Math.max(this.dataset1.getSeriesCount(), this.dataset2
    .getSeriesCount());
  for (var i = this.dataset1.getSeriesCount(); i < maxSeriesCount; i++) {
    this.dataset1.addSeries({
      name: this.dataset2.getName(i)
    });
  }
  for (var i = this.dataset2.getSeriesCount(); i < maxSeriesCount; i++) {
    this.dataset2.addSeries({
      name: this.dataset1.getName(i)
    });
  }

  this.rowMetadata = new phantasus.JoinedMetadataModel(this.dataset1
    .getRowMetadata(), !metadataInDifferentOrder ? this.dataset2
    .getRowMetadata() : new phantasus.MetadataModelColumnView(
    this.dataset2.getRowMetadata(), dataset2RowMetadataOrder));
};
phantasus.JoinedDataset.prototype = {
  getName: function (seriesIndex) {
    return this.dataset1.getName(seriesIndex);
  },
  setName: function (seriesIndex, name) {
    this.dataset1.setName(seriesIndex, name);
  },
  getDataType: function (seriesIndex) {
    return this.dataset1.getDataType(seriesIndex);
  },
  getDatasets: function () {
    return [this.dataset1, this.dataset2];
  },
  getDataset1: function () {
    return this.dataset1;
  },
  getRowMetadata: function () {
    return this.rowMetadata;
  },
  getColumnMetadata: function () {
    return this.dataset1.getColumnMetadata();
  },
  getRowCount: function () {
    return this.dataset1.getRowCount() + this.dataset2.getRowCount();
  },
  getColumnCount: function () {
    return this.dataset1.getColumnCount();
  },
  getValue: function (i, j, seriesIndex) {
    return i < this.dataset1.getRowCount() ? this.dataset1.getValue(i, j,
      seriesIndex) : this.dataset2.getValue(i
      - this.dataset1.getRowCount(), j, seriesIndex);
  },
  setValue: function (i, j, value, seriesIndex) {
    i < this.dataset1.getRowCount() ? this.dataset1.setValue(i, j, value,
      seriesIndex) : this.dataset2.setValue(i
      - this.dataset1.getRowCount(), j, value, seriesIndex);
  },
  getSeriesCount: function () {
    return this.dataset1.getSeriesCount();
  },
  addSeries: function (options) {
    this.dataset1.addSeries(options);
    return this.dataset2.addSeries(options);
  },
  removeSeries: function (seriesIndex) {
    this.dataset1.removeSeries(seriesIndex);
    this.dataset2.removeSeries(seriesIndex);
  },
  toString: function () {
    return this.getName();
  }
};
phantasus.SlicedDatasetWithNulls = function (dataset, columnIndices, columnCount,
                                            columnMetadata) {
  phantasus.DatasetAdapter.call(this, dataset);
  this.columnIndices = columnIndices;
  this.columnCount = columnCount;
  this.columnMetadata = columnMetadata;
};
phantasus.SlicedDatasetWithNulls.prototype = {
  getColumnMetadata: function () {
    return this.columnMetadata;
  },
  getColumnCount: function () {
    return this.columnCount;
  },
  getValue: function (i, j, seriesIndex) {
    var index = this.columnIndices[j];
    return index === undefined ? undefined : this.dataset.getValue(i,
      index, seriesIndex);
  },
  setValue: function (i, j, value, seriesIndex) {
    var index = this.columnIndices[j];
    if (index !== undefined) {
      this.dataset.setValue(i, index, value, seriesIndex);
    } else {
      // console.log(j + ' out of range');
    }
  }
};
phantasus.Util.extend(phantasus.SlicedDatasetWithNulls, phantasus.DatasetAdapter);
phantasus.JoinedVector = function (v1, v2) {
  this.v1 = v1;
  this.v2 = v2;
  phantasus.VectorAdapter.call(this, v1);
  this.properties = new phantasus.Map();
};
phantasus.JoinedVector.prototype = {
  setValue: function (i, value) {
    i < this.v1.size() ? this.v1.setValue(i, value) : this.v2.setValue(i
      - this.v1.size(), value);
  },
  getValue: function (i) {
    return i < this.v1.size() ? this.v1.getValue(i) : this.v2.getValue(i
      - this.v1.size());
  },
  size: function () {
    return this.v1.size() + this.v2.size();
  },
  getProperties: function () {
    return this.properties;
  }
};
phantasus.Util.extend(phantasus.JoinedVector, phantasus.VectorAdapter);
phantasus.JoinedMetadataModel = function (m1, m2) {
  this.m1 = m1;
  this.m2 = m2;
  this.vectors = [];
  for (var i = 0, count = m1.getMetadataCount(); i < count; i++) {
    var v1 = this.m1.get(i);
    var v2 = this.m2.get(i);
    var v = new phantasus.JoinedVector(v1, v2);
    // copy properties
    v1.getProperties().forEach(function (val, key) {
      if (!phantasus.VectorKeys.COPY_IGNORE.has(key)) {
        v.properties.set(key, val);
      }
    });
    v2.getProperties().forEach(function (val, key) {
      if (!phantasus.VectorKeys.COPY_IGNORE.has(key)) {
        v.properties.set(key, val);
      }
    });

    this.vectors.push(v);
  }
};
phantasus.JoinedMetadataModel.prototype = {
  add: function (name) {
    var index = phantasus.MetadataUtil.indexOf(this, name);
    var oldVector;
    if (index !== -1) {
      oldVector = this.remove(index);
    }
    var v = new phantasus.Vector(name, this.getItemCount());
    if (oldVector != null) {
      // copy properties
      oldVector.getProperties().forEach(function (val, key) {
        v.getProperties().set(key, val);
      });
      // copy values
      for (var i = 0, size = oldVector.size(); i < size; i++) {
        v.setValue(i, oldVector.getValue(i));
      }
    }
    this.vectors.push(v);
    return v;
  },
  getItemCount: function () {
    return this.m1.getItemCount() + this.m2.getItemCount();
  },
  get: function (index) {
    return this.vectors[index];
  },
  remove: function (index) {
    return this.vectors.splice(index, 1)[0];
  },
  getByName: function (name) {
    for (var i = 0, length = this.vectors.length; i < length; i++) {
      if (name === this.vectors[i].getName()) {
        return this.vectors[i];
      }
    }
  },
  getMetadataCount: function () {
    return this.vectors.length;
  }
};

/**
 *
 *@implements {phantasus.MetadataModelInterface}
 */
phantasus.MetadataModelAdapter = function (model) {
  this.model = model;
};
phantasus.MetadataModelAdapter.prototype = {
  add: function (name) {
    return this.model.add(name);
  },
  getItemCount: function () {
    return this.model.getItemCount();
  },
  get: function (index) {
    return this.model.get(index);
  },
  remove: function (index) {
    return this.model.remove(index);
  },
  getByName: function (name) {
    return this.model.getByName(name);
  },
  getMetadataCount: function () {
    return this.model.getMetadataCount();
  }
};

phantasus.MetadataModelColumnView = function (model, indices) {
  this.model = model;
  this.indices = indices;
};
phantasus.MetadataModelColumnView.prototype = {
  add: function (name) {
    var vector = this.model.add(name);
    var index = phantasus.MetadataUtil.indexOf(this.model, name);
    this.indices.push(index);
    return vector;
  },
  getMetadataCount: function () {
    return this.indices.length;
  },
  get: function (index) {
    if (index < 0 || index >= this.indices.length) {
      throw 'index out of bounds';
    }
    return this.model.get(this.indices[index]);
  },
  remove: function (index) {
    if (index < 0 || index >= this.indices.length) {
      throw 'index out of bounds';
    }
    var v = this.model.remove(this.indices[index]);
    this.indices.splice(index, 1);
    return v;
  },
  getByName: function (name) {
    var index = phantasus.MetadataUtil.indexOf(this, name);
    return index !== -1 ? this.get(index) : undefined;
  }
};
phantasus.Util.extend(phantasus.MetadataModelColumnView,
  phantasus.MetadataModelAdapter);

/**
 * Stores annotations for the rows or columns of a dataset.
 * @interface phantasus.MetadataModelInterface
 *
 */

/**
 * Appends the specified vector to this meta data. If an existing vector
 * with the same name already exists, it is removed and existing properties
 * and values copied to the new vector before appending the new vector.
 * @function
 * @name phantasus.MetadataModelInterface#add
 * @param name {String} The vector name to be inserted into this meta data instance.
 * @param options {object}
 * @return {phantasus.VectorInterface} the added vector.
 */

/**
 * Returns the number of items that a vector in this meta data model
 * contains.
 *
 * @function
 * @name phantasus.MetadataModelInterface#getItemCount
 * @return {number} the item count
 */

/**
 * Returns the vector at the specified metadata index.
 *
 * @function
 * @name phantasus.MetadataModelInterface#get
 * @param index {number} the metadata index
 * @return {phantasus.VectorInterface} the vector
 */

/**
 * Removes the column at the specified position in this meta data instance
 * Shifts any subsequent columns to the left (subtracts one from their
 * indices).
 *
 * @function
 * @name phantasus.MetadataModelInterface#remove
 * @param index {number} the meta data index to remove.
 * @return {phantasus.VectorInterface} the removed vector
 * @throws Error if index < 0 or >= getMetadataCount
 */

/**
 * Returns the vector witht the specified name.
 *
 * @function
 * @name phantasus.MetadataModelInterface#getByName
 * @param name {string} the vector name
 * @return {phantasus.VectorInterface} the vector
 */

/**
 * Returns the number of vectors in this meta data instance.
 *
 * @function
 * @name phantasus.MetadataModelInterface#getMetadataCount
 * @return {number} the number of vectors.
 */



phantasus.MetadataModelItemView = function (model, indices) {
  this.model = model;
  this.indices = indices;
};
phantasus.MetadataModelItemView.prototype = {
  add: function (name) {
    var v = this.model.add(name);
    return new phantasus.SlicedVector(v, this.indices);
  },
  getItemCount: function () {
    return this.indices.length;
  },
  get: function (index) {
    var v = this.model.get(index);
    if (v === undefined) {
      return undefined;
    }
    return new phantasus.SlicedVector(v, this.indices);
  },
  getByName: function (name) {
    var v = this.model.getByName(name);
    if (v === undefined) {
      return undefined;
    }
    return new phantasus.SlicedVector(v, this.indices);
  },
  getMetadataCount: function () {
    return this.model.getMetadataCount();
  }
};
phantasus.Util.extend(phantasus.MetadataModelItemView,
  phantasus.MetadataModelAdapter);

/**
 * Creates a new meta data model instance.
 *
 * @param itemCount {number}
 *            the number of items that vectors in this instances will hold.
 * @implements {phantasus.MetadataModelInterface}
 * @constructor
 */
phantasus.MetadataModel = function (itemCount) {
  this.itemCount = itemCount;
  this.vectors = [];
};
phantasus.MetadataModel.prototype = {
  add: function (name, options) {
    var index = phantasus.MetadataUtil.indexOf(this, name);
    var oldVector;
    if (index !== -1) {
      oldVector = this.get(index);
    }
    var v = new phantasus.Vector(name, this.getItemCount());
    if (oldVector != null) {
      // copy values
      for (var i = 0, size = oldVector.size(); i < size; i++) {
        var val = oldVector.getValue(i);
        v.setValue(i, val);
      }
    }
    if (index !== -1) {
      // replace old vector
      this.vectors.splice(index, 1, v);
    } else {
      this.vectors.push(v);
    }
    return v;
  },
  getItemCount: function () {
    return this.itemCount;
  },
  get: function (index) {
    return this.vectors[index];
  },
  remove: function (index) {
    return this.vectors.splice(index, 1)[0];
  },
  getByName: function (name) {
    var index = phantasus.MetadataUtil.indexOf(this, name);
    return index !== -1 ? this.get(index) : undefined;
  },
  getMetadataCount: function () {
    return this.vectors.length;
  }
};

phantasus.MetadataUtil = function () {
};

phantasus.MetadataUtil.renameFields = function (dataset, options) {
  _.each(options.rows, function (item) {
    if (item.renameTo) {
      var v = dataset.getRowMetadata().getByName(item.field);
      if (v) {
        v.setName(item.renameTo);
      }
    }
  });
  _.each(options.columns, function (item) {
    if (item.renameTo) {
      var v = dataset.getColumnMetadata().getByName(item.field);
      if (v) {
        v.setName(item.renameTo);
      }
    }
  });
};

/**
 * @param options.model
 *            Metadata model of currently visible tracks
 * @param options.fullModel
 *            Metadata model of all metadata tracks
 * @param options.text
 *            Search text
 * @param options.isColumns
 *            Whether to search columns
 * @param options.defaultMatchMode
 *            'exact' or 'contains'
 * @param options.matchAllPredicates Whether to match all predicates
 *
 */
phantasus.MetadataUtil.search = function (options) {
  var model = options.model;
  var fullModel = options.fullModel;
  if (!fullModel) {
    fullModel = model;
  }
  var text = options.text;
  var isColumns = options.isColumns;
  text = $.trim(text);
  if (text === '') {
    return null;
  }
  var tokens = phantasus.Util.getAutocompleteTokens(text);
  if (tokens.length == 0) {
    return null;
  }
  var indexField = '#';
  var fieldNames = phantasus.MetadataUtil.getMetadataNames(fullModel);
  fieldNames.push(indexField);
  var predicates = phantasus.Util.createSearchPredicates({
    tokens: tokens,
    fields: fieldNames,
    defaultMatchMode: options.defaultMatchMode
  });
  var vectors = [];
  var nameToVector = new phantasus.Map();
  for (var j = 0; j < fullModel.getMetadataCount(); j++) {
    var v = fullModel.get(j);
    var dataType = phantasus.VectorUtil.getDataType(v);
    var wrapper = {
      vector: v,
      dataType: dataType,
      isArray: dataType.indexOf('[') === 0
    };
    nameToVector.set(v.getName(), wrapper);
    if (model.getByName(v.getName()) != null) {
      vectors.push(wrapper);
    }

  }
  // TODO only search numeric fields for range searches
  var indices = [];
  var npredicates = predicates.length;
  for (var p = 0; p < npredicates; p++) {
    var predicate = predicates[p];
    var filterColumnName = predicate.getField();
    if (filterColumnName != null && !predicate.isNumber()) {
      var wrapper = nameToVector.get(filterColumnName);
      if (wrapper && (wrapper.dataType === 'number' || wrapper.dataType === '[number]')) {
        if (predicate.getText) {
          predicates[p] = new phantasus.Util.EqualsPredicate(filterColumnName, parseFloat(predicate.getText()));
        } else if (predicate.getValues) {
          var values = [];
          predicate.getValues().forEach(function (val) {
            values.push(parseFloat(val));
          });
          predicate[p] = new phantasus.Util.ExactTermsPredicate(filterColumnName, values);
        }
      }
    }

  }

  var matchAllPredicates = options.matchAllPredicates === true;

  function isPredicateMatch(predicate) {
    var filterColumnName = predicate.getField();
    if (filterColumnName != null) {
      var value = null;
      if (filterColumnName === indexField) {
        value = i + 1;
        if (predicate.accept(value)) {
          return true;
        }
      } else {
        var wrapper = nameToVector.get(filterColumnName);
        if (wrapper) {
          value = wrapper.vector.getValue(i);
          if (wrapper.isArray) {
            if (value != null) {
              for (var k = 0; k < value.length; k++) {
                if (predicate.accept(value[k])) {
                  return true;

                }
              }
            }
          } else {
            if (predicate.accept(value)) {
              return true;
            }
          }

        }
      }

    }
    else { // try all fields
      for (var j = 0; j < nfields; j++) {
        var wrapper = vectors[j];
        var value = wrapper.vector.getValue(i);

        if (wrapper.isArray) {
          if (value != null) {
            for (var k = 0; k < value.length; k++) {
              if (predicate.accept(value[k])) {
                return true;
              }
            }
          }
        } else {
          if (predicate.accept(value)) {
            return true;
          }
        }

      }
    }

  }

  var nfields = vectors.length;
  for (var i = 0, nitems = model.getItemCount(); i < nitems; i++) {
    if (!matchAllPredicates) { // at least one predicate matches
      for (var p = 0; p < npredicates; p++) {
        var predicate = predicates[p];
        if (isPredicateMatch(predicate)) {
          indices.push(i);
          break;
        }
      }
    } else {
      var matches = true;
      for (var p = 0; p < npredicates; p++) {
        var predicate = predicates[p];
        if (!isPredicateMatch(predicate)) {
          matches = false;
          break;
        }
      }
      if (matches) {
        indices.push(i);
      }
    }

  }
  return indices;
};

phantasus.MetadataUtil.shallowCopy = function (model) {
  var copy = new phantasus.MetadataModel(model.getItemCount());
  for (var i = 0; i < model.getMetadataCount(); i++) {
    var v = model.get(i);
    // copy properties b/c they can be modified via ui
    var newVector = new phantasus.VectorAdapter(v);
    newVector.properties = new phantasus.Map();
    newVector.getProperties = function () {
      return this.properties;
    };

    v.getProperties().forEach(function (val, key) {
      if (!phantasus.VectorKeys.COPY_IGNORE.has(key)) {
        newVector.properties.set(key, val);
      }

    });

    copy.vectors.push(newVector);
  }
  return copy;
};
phantasus.MetadataUtil.autocomplete = function (model) {
  return function (tokens, cb) {
    // check for term:searchText
    var matches = [];
    var regex = null;
    var regexMatch = null;
    var searchModel = model;
    var token = tokens != null && tokens.length > 0 ? tokens[tokens.selectionStartIndex]
      : '';
    token = $.trim(token);
    var fieldSearchFieldName = null;
    if (token !== '') {
      var semi = token.indexOf(':');
      if (semi > 0) { // field search?
        if (token.charCodeAt(semi - 1) !== 92) { // \:
          var possibleField = $.trim(token.substring(0, semi));
          if (possibleField.length > 0
            && possibleField[0] === '"'
            && possibleField[token.length - 1] === '"') {
            possibleField = possibleField.substring(1,
              possibleField.length - 1);
          }
          var index = phantasus.MetadataUtil.indexOf(searchModel,
            possibleField);
          if (index !== -1) {
            fieldSearchFieldName = possibleField;
            token = $.trim(token.substring(semi + 1));
            searchModel = new phantasus.MetadataModelColumnView(
              model, [index]);
          }
        }

      }
      var set = new phantasus.Set();
      // regex used to determine if a string starts with substring `q`

      regex = new RegExp(phantasus.Util.escapeRegex(token), 'i');
      regexMatch = new RegExp('(' + phantasus.Util.escapeRegex(token) + ')', 'i');
      // iterate through the pool of strings and for any string that
      // contains the substring `q`, add it to the `matches` array
      var max = 10;

      var vectors = [];
      var isArray = [];
      for (var j = 0; j < searchModel.getMetadataCount(); j++) {
        var v = searchModel.get(j);
        var dataType = phantasus.VectorUtil.getDataType(v);
        if (dataType === 'string' || dataType === '[string]') { // skip
          // numeric
          // fields
          vectors.push(v);
          isArray.push(dataType === '[string]');
        }
      }

      var nfields = vectors.length;

      loop: for (var i = 0, nitems = searchModel.getItemCount(); i < nitems; i++) {
        for (var j = 0; j < nfields; j++) {
          var v = vectors[j];
          var val = v.getValue(i);
          if (val != null) {
            if (isArray[j]) {
              for (var k = 0; k < val.length; k++) {
                var id = new phantasus.Identifier([val[k],
                  v.getName()]);
                if (!set.has(id) && regex.test(val[k])) {
                  set.add(id);
                  if (set.size() === max) {
                    break loop;
                  }
                }
              }
            } else {
              var id = new phantasus.Identifier([val,
                v.getName()]);
              if (!set.has(id) && regex.test(val)) {
                set.add(id);
                if (set.size() === max) {
                  break loop;
                }
              }
            }
          }

        }
      }

      set.forEach(function (id) {
        var array = id.getArray();
        var field = array[1];
        var val = array[0];
        var quotedField = field;
        if (quotedField.indexOf(' ') !== -1) {
          quotedField = '"' + quotedField + '"';
        }
        var quotedValue = val;
        if (quotedValue.indexOf(' ') !== -1) {
          quotedValue = '"' + quotedValue + '"';
        }
        matches.push({
          value: quotedField + ':' + quotedValue,
          label: '<span style="font-weight:300;">' + field
          + ':</span>'
          + '<span>' + val.replace(regexMatch, '<b>$1</b>')
          + '</span>'
        });

      });
    }

    // field names
    if (regex == null) {
      regex = new RegExp('.*', 'i');
    }

    for (var j = 0; j < searchModel.getMetadataCount(); j++) {
      var v = searchModel.get(j);
      var dataType = phantasus.VectorUtil.getDataType(v);
      var field = v.getName();
      if (dataType === 'number' || dataType === 'string'
        || dataType === '[string]') {
        if (regex.test(field) && field !== fieldSearchFieldName) {
          var quotedField = field;
          if (quotedField.indexOf(' ') !== -1) {
            quotedField = '"' + quotedField + '"';
          }
          matches.push({
            value: quotedField + ':',
            label: '<span style="font-weight:300;">' + (regexMatch == null ? field : field.replace(regexMatch, '<b>$1</b>'))
            + ':</span>' + (dataType === 'number' ? ('<span' +
            ' style="font-weight:300;font-size:85%;">.., >, <, >=, <=,' +
            ' =</span>') : ''),
            show: true
          });
        }
      }
    }
    cb(matches);
  };
};

phantasus.MetadataUtil.getMetadataNames = function (metadataModel, unsorted) {
  var names = [];
  for (var i = 0, count = metadataModel.getMetadataCount(); i < count; i++) {
    names.push(metadataModel.get(i).getName(i));
  }

  if (!unsorted) {
    names.sort(function (a, b) {
      a = a.toLowerCase();
      b = b.toLowerCase();
      return (a < b ? -1 : (a === b ? 0 : 1));
    });
  }
  return names;
};

phantasus.MetadataUtil.getMetadataSignedNumericFields = function (metadataModel) {
  var fields = [];
  for (var i = 0, count = metadataModel.getMetadataCount(); i < count; i++) {
    var field = metadataModel.get(i);
    var properties = field.getProperties();
    if (properties.get('phantasus.dataType') === 'number') {
      var hasPositive = false;
      var hasNegative = false;
      for (var j = 0; j < field.size(); j++) {
          if (field.getValue(j) > 0) {
              hasPositive = true;
          }
          if (field.getValue(j) < 0) {
              hasNegative = true;
          }
          if (hasPositive && hasNegative) {
              break;
          }

      }
      if (hasPositive && hasNegative) {
        fields.push(field);
      }
     
    }
  }

  return fields;
};

phantasus.MetadataUtil.getVectors = function (metadataModel, names) {
  var vectors = [];
  names.forEach(function (name) {
    var v = metadataModel.getByName(name);
    if (!v) {
      throw name + ' not found. Available fields are '
      + phantasus.MetadataUtil.getMetadataNames(metadataModel);
    }
    vectors.push(v);
  });
  return vectors;
};
phantasus.MetadataUtil.indexOf = function (metadataModel, name) {
  for (var i = 0, length = metadataModel.getMetadataCount(); i < length; i++) {
    if (name === metadataModel.get(i).getName()) {
      return i;
    }
  }
  return -1;
};

phantasus.MetadataUtil.DEFAULT_STRING_ARRAY_FIELDS = ['target', 'gene_target', 'moa'];

phantasus.MetadataUtil.DEFAULT_HIDDEN_FIELDS = new phantasus.Set();
['pr_analyte_id', 'pr_gene_title', 'pr_gene_id', 'pr_analyte_num',
  'pr_bset_id', 'pr_lua_id', 'pr_pool_id', 'pr_is_bing', 'pr_is_inf',
  'pr_is_lmark', 'qc_slope', 'qc_f_logp', 'qc_iqr', 'bead_batch',
  'bead_revision', 'bead_set', 'det_mode', 'det_plate', 'det_well',
  'mfc_plate_dim', 'mfc_plate_id', 'mfc_plate_name', 'mfc_plate_quad',
  'mfc_plate_well', 'pert_dose_unit', 'pert_id_vendor', 'pert_mfc_desc',
  'pert_mfc_id', 'pert_time', 'pert_time_unit', 'pert_univ_id',
  'pert_vehicle', 'pool_id', 'rna_plate', 'rna_well', 'count_mean',
  'count_cv', 'provenance_code'].forEach(function (name) {
  phantasus.MetadataUtil.DEFAULT_HIDDEN_FIELDS.add(name);
});

phantasus.MetadataUtil.maybeConvertStrings = function (metadata,
                                                      metadataStartIndex) {
  for (var i = metadataStartIndex, count = metadata.getMetadataCount(); i < count; i++) {
    phantasus.VectorUtil.maybeConvertStringToNumber(metadata.get(i));
  }
  phantasus.MetadataUtil.DEFAULT_STRING_ARRAY_FIELDS.forEach(function (field) {
    if (metadata.getByName(field)) {
      phantasus.VectorUtil.maybeConvertToStringArray(metadata
        .getByName(field), ',');
    }
  });

};
phantasus.MetadataUtil.copy = function (src, dest) {
  if (src.getItemCount() != dest.getItemCount()) {
    throw 'Item count not equal in source and destination. '
    + src.getItemCount() + ' != ' + dest.getItemCount();
  }
  var itemCount = src.getItemCount();
  var metadataColumns = src.getMetadataCount();
  for (var j = 0; j < metadataColumns; j++) {
    var srcVector = src.get(j);
    var destVector = dest.getByName(srcVector.getName());
    if (destVector == null) {
      destVector = dest.add(srcVector.getName());
    }
    for (var i = 0; i < itemCount; i++) {
      destVector.setValue(i, srcVector.getValue(i));
    }
  }
};
phantasus.MetadataUtil.addVectorIfNotExists = function (metadataModel, name) {
  var v = metadataModel.getByName(name);
  if (!v) {
    v = metadataModel.add(name);
  }
  return v;
};
phantasus.MetadataUtil.getMatchingIndices = function (metadataModel, tokens) {
  var indices = {};
  for (var itemIndex = 0, nitems = metadataModel.getItemCount(); itemIndex < nitems; itemIndex++) {
    var matches = false;
    for (var metadataIndex = 0, metadataCount = metadataModel
      .getMetadataCount(); metadataIndex < metadataCount && !matches; metadataIndex++) {
      var vector = metadataModel.get(metadataModel
        .getColumnName(metadataIndex));
      var value = vector.getValue(itemIndex);
      for (var i = 0, length = tokens.length; i < length; i++) {
        if (tokens[i] == value) {
          matches = true;
          break;
        }
      }
    }
    if (matches) {
      indices[itemIndex] = 1;
    }
  }
  return indices;
};

phantasus.MolarConcentration = function () {

};
/*
 *
 millimolar 	mM 	10-3 molar 	10-0 mol/m3
 micromolar 	uM 	10-6 molar 	10-3 mol/m3
 nanomolar 	nM 	10-9 molar 	10-6 mol/m3
 picomolar 	pM 	10-12 molar 	10-9 mol/m3
 femtomolar 	fM 	10-15 molar 	10-12 mol/m3
 attomolar 	aM 	10-18 molar 	10-15 mol/m3
 zeptomolar 	zM 	10-21 molar 	10-18 mol/m3
 yoctomolar 	yM[3] 	10-24 molar	10-27 mol/m3
 */
phantasus.MolarConcentration.getMicroMolarConcentration = function (text) {
  /** concentration in molar*/
  text = text.toLowerCase();
  for (var i = 0; i < phantasus.MolarConcentration.CONCENTRATIONS.length; i++) {
    var pair = phantasus.MolarConcentration.CONCENTRATIONS[i];
    var key = pair[0];
    var factorToMolar = pair[1];
    var index = text.indexOf(key);
    if (index != -1) {
      var value = text.substring(0, index).trim();
      var factor = factorToMolar / 10E6;
      var conc = parseFloat(value);
      return conc / factor;

    }
  }
};
phantasus.MolarConcentration.CONCENTRATIONS = [
  ['mm', 10E3],
  ['um', 10E6],
  ['\u00B5' + 'm', 10E6],
  ['nm', 10E9],
  ['pm', 10E12],
  ['fm', 10E15],
  ['am', 10E18],
  ['zm', 10E21],
  ['ym', 10E24],
  ['m', 1]];





phantasus.MaximumMeanProbe = function(probes) {
  return phantasus.MaximumUnivariateFunction(probes, phantasus.Mean);
};

phantasus.MaximumMeanProbe.toString = function() {
  return "Maximum Mean Probe";
};

phantasus.MaximumMeanProbe.selectOne = true;

phantasus.MaximumMedianProbe = function(probes) {
  return phantasus.MaximumUnivariateFunction(probes, phantasus.Median);
};

phantasus.MaximumMedianProbe.toString = function() {
  return "Maximum Median Probe";
};

phantasus.MaximumMedianProbe.selectOne = true;

phantasus.MaximumUnivariateFunction = function(rowView, fun) {
  // console.log("MaximumUnivariateFunction args", rowView, fun);
  var curMax = Number.NEGATIVE_INFINITY;
  var curIndex = -1;
  for (var i = 0; i < rowView.dataset.getRowCount(); i++) {
    rowView.setIndex(i);
    var mean = fun(rowView);
    if (mean > curMax) {
      curMax = mean;
      curIndex = i;
    }
  }
  return {
    value : curMax,
    index : curIndex
  }
};

phantasus.Positions = function () {
  this.spaces = undefined;
  this.defaultPositionFunction = function (index) {
    return (this.size * index);
  };
  this.squishedPositionFunction = function (index) {
    return this.positions[index];
  };
  this.positionFunction = this.defaultPositionFunction;
  this.squishedIndices = {};
  this.isSquished = false;
};
phantasus.Positions.getBottom = function (rect, rowPositions) {
  var bottom = rowPositions.getLength();
  if (rect != null) {
    bottom = 1 + rowPositions.getIndex(rect.y + rect.height, false);
    bottom = Math.max(0, bottom);
    bottom = Math.min(rowPositions.getLength(), bottom);
  }
  return bottom;
};
phantasus.Positions.getTop = function (rect, rowPositions) {
  var top = 0;
  if (rect != null) {
    top = rowPositions.getIndex(rect.y, false) - 1;
    top = Math.max(0, top);
    top = Math.min(rowPositions.getLength(), top);
  }
  return top;
};
phantasus.Positions.getLeft = function (rect, columnPositions) {
  var left = 0;
  if (rect != null) {
    left = columnPositions.getIndex(rect.x, false) - 1;
    left = Math.max(0, left);
    left = Math.min(columnPositions.getLength(), left);
  }
  return left;
};
phantasus.Positions.getRight = function (rect, columnPositions) {
  var right = columnPositions.getLength();
  if (rect != null) {
    right = 1 + columnPositions.getIndex(rect.x + rect.width, false);
    right = Math.min(columnPositions.getLength(), right);
  }
  return right;
};
phantasus.Positions.prototype = {
  length: 0,
  size: 13,
  squishFactor: 0.1,
  compress: true,
  copy: function () {
    var copy = new phantasus.Positions();
    if (this.spaces) {
      copy.spaces = this.spaces.slice();
    }
    copy.compress = this.compress;
    copy.squishFactor = this.squishFactor;
    copy.size = this.size;
    copy.length = this.length;
    if (this.isSquished) {
      copy.positionFunction = copy.squishedPositionFunction;
      copy.squishedIndices = _.clone(this.squishedIndices);
      copy.isSquished = true;
    }
    return copy;
  },
  getIndex: function (position, exact) {
    if (this.getLength() === 0) {
      return -1;
    }
    if (exact) {
      return this.binaryExactSearch(position);
    } else {
      return this.binaryInExactSearch(position);
    }
  },
  getLength: function () {
    return this.length;
  },
  getPosition: function (index) {
    return this.positionFunction(index)
      + (this.spaces !== undefined ? this.spaces[index] : 0);
  },
  getItemSize: function (index) {
    return this.squishedIndices[index] === true ? this.size
    * this.squishFactor : this.size;
  },
  getSize: function () {
    return this.size;
  },
  setSpaces: function (spaces) {
    this.spaces = spaces;
  },
  setLength: function (length) {
    this.length = length;
    this.trigger('change', {
      source: this,
      value: 'length'
    });
  },
  setSize: function (size) {
    this.size = size;
    if (this.isSquished) {
      this.setSquishedIndices(this.squishedIndices);
    }
    this.trigger('change', {
      source: this,
      value: 'size'
    });
  },
  setSquishedIndices: function (squishedIndices) {
    if (squishedIndices != null) {
      var compress = this.compress;
      this.squishedIndices = squishedIndices;
      var positions = [];
      var squishFactor = this.squishFactor;
      var size = this.size;
      var position = 0;
      for (var i = 0, length = this.getLength(); i < length; i++) {
        var itemSize;
        if (squishedIndices[i] === true) {
          positions.push(position);
          itemSize = size * squishFactor;
          position += itemSize;
        } else {
          if (!compress) {
            position = size * i;
          }
          positions.push(position);
          position += size;
        }
      }
      this.isSquished = true;
      this.positions = positions;
      this.positionFunction = this.squishedPositionFunction;
    } else {
      this.squishedIndices = {};
      this.isSquished = false;
      this.positionFunction = this.defaultPositionFunction;
    }
    this.trigger('change', {
      source: this,
      value: 'squishedIndices'
    });
  },
  setSquishFactor: function (f) {
    if (this.squishFactor !== f) {
      this.squishFactor = f;
      if (this.isSquished) {
        this.setSquishedIndices(this.squishedIndices);
      }
      this.trigger('change', {
        source: this,
        value: 'squishFactor'
      });
    }
  },
  getSquishFactor: function () {
    return this.squishFactor;
  },
  binaryExactSearch: function (position) {
    var low = 0;
    var high = this.length - 1;
    while (low <= high) {
      var mid = (low + high) >> 1;
      var midVal = this.getPosition(mid);
      var size = this.getItemSize(mid);
      if (midVal <= position && position < (midVal + size)) {
        return mid;
      }
      if (midVal < position) {
        low = mid + 1;
      } else if (midVal > position) {
        high = mid - 1;
      } else {
        return mid;
        // key found
      }
    }
    return -1;
    // key not found
  },
  binaryInExactSearch: function (position) {
    var low = 0;
    var high = this.getLength() - 1;
    var maxIndex = this.getLength() - 1;
    if (position <= this.getPosition(0)) {
      return 0;
    }
    while (low <= high) {
      var mid = (low + high) >> 1;
      var midVal = this.getPosition(mid);
      var size = this.getItemSize(mid);
      var nextStart = maxIndex === mid ? midVal + size : this
      .getPosition(mid + 1);
      if (midVal <= position && position < nextStart) {
        return mid;
      }
      if (midVal < position) {
        low = mid + 1;
      } else if (midVal > position) {
        high = mid - 1;
      } else {
        return mid;
        // key found
      }
    }
    return low - 1;
    // key not found
  }
};

phantasus.Util.extend(phantasus.Positions, phantasus.Events);

/**
 *
 * @param dataset
 * @constructor
 */
phantasus.Project = function (dataset) {
  this.originalDataset = dataset;
  this.rowIndexMapper = new phantasus.IndexMapper(this, true);
  this.columnIndexMapper = new phantasus.IndexMapper(this, false);
  this.groupRows = [];
  this.groupColumns = [];
  this.rowColorModel = new phantasus.VectorColorModel();
  this.columnColorModel = new phantasus.VectorColorModel();
  this.rowShapeModel = new phantasus.VectorShapeModel();
  this.columnShapeModel = new phantasus.VectorShapeModel();
  this.rowFontModel = new phantasus.VectorFontModel();
  this.columnFontModel = new phantasus.VectorFontModel();
  this.hoverColumnIndex = -1;
  this.hoverRowIndex = -1;
  this.columnSelectionModel = new phantasus.SelectionModel(this, true);
  this.rowSelectionModel = new phantasus.SelectionModel(this, false);
  this.elementSelectionModel = new phantasus.ElementSelectionModel(this);
  this.symmetricProjectListener = null;
  phantasus.Project._recomputeCalculatedColumnFields(this.originalDataset, phantasus.VectorKeys.RECOMPUTE_FUNCTION_NEW_HEAT_MAP);
  phantasus.Project
    ._recomputeCalculatedColumnFields(new phantasus.TransposedDatasetView(
      this.originalDataset), phantasus.VectorKeys.RECOMPUTE_FUNCTION_NEW_HEAT_MAP);
};
phantasus.Project.Events = {
  DATASET_CHANGED: 'datasetChanged',
  ROW_GROUP_BY_CHANGED: 'rowGroupByChanged',
  COLUMN_GROUP_BY_CHANGED: 'columnGroupByChanged',
  ROW_FILTER_CHANGED: 'rowFilterChanged',
  COLUMN_FILTER_CHANGED: 'columnFilterChanged',
  ROW_SORT_ORDER_CHANGED: 'rowSortOrderChanged',
  COLUMN_SORT_ORDER_CHANGED: 'columnSortOrderChanged',
  ROW_TRACK_REMOVED: 'rowTrackRemoved',
  COLUMN_TRACK_REMOVED: 'columnTrackRemoved'
};

phantasus.Project._recomputeCalculatedColumnFields = function (dataset, key) {
  var metadata = dataset.getColumnMetadata();
  var view = new phantasus.DatasetColumnView(dataset);
  var nfound = 0;
  for (var metadataIndex = 0,
         count = metadata.getMetadataCount(); metadataIndex < count; metadataIndex++) {
    var vector = metadata.get(metadataIndex);
    if (vector.getProperties().get(phantasus.VectorKeys.FUNCTION) != null
      && vector.getProperties().get(key)) {

      // // copy properties
      // var v = metadata.add(name);
      // vector.getProperties().forEach(function (val, key) {
      //   v.getProperties().set(key, val);
      // });
      // vector = v;
      var f = phantasus.VectorUtil.jsonToFunction(vector, phantasus.VectorKeys.FUNCTION);
      for (var j = 0, size = vector.size(); j < size; j++) {
        view.setIndex(j);
        vector.setValue(j, f(view, dataset, j));
      }
      nfound++;
    }
  }
  return nfound;
};
phantasus.Project.prototype = {
  isSymmetric: function () {
    return this.symmetricProjectListener != null;
  },
  setSymmetric: function (heatMap) {
    if (heatMap != null) {
      if (this.symmetricProjectListener == null) {
        this.symmetricProjectListener = new phantasus.SymmetricProjectListener(heatMap.getProject(), heatMap.vscroll, heatMap.hscroll);
      }
    } else {
      if (this.symmetricProjectListener != null) {
        this.symmetricProjectListener.dispose();
      }
      this.symmetricProjectListener = null;
    }
  },
  getHoverColumnIndex: function () {
    return this.hoverColumnIndex;
  },
  setHoverColumnIndex: function (index) {
    this.hoverColumnIndex = index;
  },
  getHoverRowIndex: function () {
    return this.hoverRowIndex;
  },
  setHoverRowIndex: function (index) {
    this.hoverRowIndex = index;
  },
  getRowColorModel: function () {
    return this.rowColorModel;
  },
  getRowShapeModel: function () {
    return this.rowShapeModel;
  },
  getColumnShapeModel: function () {
    return this.columnShapeModel;
  },
  getRowFontModel: function () {
    return this.rowFontModel;
  },
  getColumnFontModel: function () {
    return this.columnFontModel;
  },
  getGroupRows: function () {
    return this.groupRows;
  },
  getGroupColumns: function () {
    return this.groupColumns;
  },
  getFullDataset: function () {
    return this.originalDataset;
  },
  getColumnSelectionModel: function () {
    return this.columnSelectionModel;
  },
  getRowSelectionModel: function () {
    return this.rowSelectionModel;
  },
  getFilteredSortedRowIndices: function () {
    return this.rowIndexMapper.convertToView();
  },
  getFilteredSortedColumnIndices: function () {
    return this.columnIndexMapper.convertToView();
  },
  getElementSelectionModel: function () {
    return this.elementSelectionModel;
  },
  setFullDataset: function (dataset, notify) {
    this.originalDataset = dataset;
    this.rowIndexMapper.setFilter(this.rowIndexMapper.getFilter());
    this.columnIndexMapper.setFilter(this.columnIndexMapper.getFilter());
    this.columnSelectionModel.clear();
    this.rowSelectionModel.clear();
    this.elementSelectionModel.clear();
    if (notify) {
      this.trigger(phantasus.Project.Events.DATASET_CHANGED);
    }
  },
  setGroupRows: function (keys, notify) {
    this.groupRows = keys;
    for (var i = 0, nkeys = keys.length; i < nkeys; i++) {
      if (keys[i].isColumns() === undefined) {
        keys[i].setColumns(false);
      }
    }
    if (notify) {
      this.trigger(phantasus.Project.Events.ROW_GROUP_BY_CHANGED);
    }
  },
  setGroupColumns: function (keys, notify) {
    this.groupColumns = keys;
    for (var i = 0, nkeys = keys.length; i < nkeys; i++) {
      if (keys[i].isColumns() === undefined) {
        keys[i].setColumns(true);
      }
    }
    if (notify) {
      this.trigger(phantasus.Project.Events.COLUMN_GROUP_BY_CHANGED);
    }
  },
  setRowFilter: function (filter, notify) {
    this._saveSelection(false);
    this.rowIndexMapper.setFilter(filter);
    this._restoreSelection(false);
    if (notify) {
      this.trigger(phantasus.Project.Events.ROW_FILTER_CHANGED);
    }
  },
  getRowFilter: function () {
    return this.rowIndexMapper.getFilter();
  },
  getColumnFilter: function () {
    return this.columnIndexMapper.getFilter();
  },
  setColumnFilter: function (filter, notify) {
    this._saveSelection(true);
    this.columnIndexMapper.setFilter(filter);
    this._restoreSelection(true);
    if (notify) {
      this.trigger(phantasus.Project.Events.COLUMN_FILTER_CHANGED);
    }
  },
  getColumnColorModel: function () {
    return this.columnColorModel;
  },
  getSortedFilteredDataset: function () {
    return phantasus.DatasetUtil.slicedView(this.getFullDataset(),
      this.rowIndexMapper.convertToView(), this.columnIndexMapper
        .convertToView());
  },
  getSelectedDataset: function (options) {
    options = $.extend({}, {
      selectedRows: true,
      selectedColumns: true,
      emptyToAll: true
    }, options);
    var dataset = this.getSortedFilteredDataset();
    var rows = null;
    if (options.selectedRows) {
      rows = this.rowSelectionModel.getViewIndices().values().sort(
        function (a, b) {
          return (a === b ? 0 : (a < b ? -1 : 1));
        });
      if (rows.length === 0 && options.emptyToAll) {
        rows = null;
      }
    }
    var columns = null;
    if (options.selectedColumns) {
      columns = this.columnSelectionModel.getViewIndices().values().sort(
        function (a, b) {
          return (a === b ? 0 : (a < b ? -1 : 1));
        });
      if (columns.length === 0 && options.emptyToAll) {
        columns = null;
      }
    }
    return rows == null && columns == null ? dataset : new phantasus.SlicedDatasetView(dataset, rows, columns);
  },
  _saveSelection: function (isColumns) {
    this.elementSelectionModel.save();
    if (isColumns) {
      this.columnSelectionModel.save();
    } else {
      this.rowSelectionModel.save();
    }
  },
  _restoreSelection: function (isColumns) {
    if (isColumns) {
      this.columnSelectionModel.restore();
    } else {
      this.rowSelectionModel.restore();
    }
    this.elementSelectionModel.restore();
  },
  setRowSortKeys: function (keys, notify) {
    this._saveSelection(false);
    for (var i = 0, nkeys = keys.length; i < nkeys; i++) {
      if (keys[i].isColumns() === undefined) {
        keys[i].setColumns(false);
      }
    }
    this.rowIndexMapper.setSortKeys(keys);
    this._restoreSelection(false);
    if (notify) {
      this.trigger(phantasus.Project.Events.ROW_SORT_ORDER_CHANGED);
    }
  },
  setColumnSortKeys: function (keys, notify) {
    this._saveSelection(true);
    for (var i = 0, nkeys = keys.length; i < nkeys; i++) {
      if (keys[i].isColumns() === undefined) {
        keys[i].setColumns(true);
      }
    }
    this.columnIndexMapper.setSortKeys(keys);
    this._restoreSelection(true);
    if (notify) {
      this.trigger(phantasus.Project.Events.COLUMN_SORT_ORDER_CHANGED);
    }
  },
  getRowSortKeys: function () {
    return this.rowIndexMapper.sortKeys;
  },
  getColumnSortKeys: function () {
    return this.columnIndexMapper.sortKeys;
  },
  convertViewColumnIndexToModel: function (viewIndex) {
    return this.columnIndexMapper.convertViewIndexToModel(viewIndex);
  },
  convertViewRowIndexToModel: function (viewIndex) {
    return this.rowIndexMapper.convertViewIndexToModel(viewIndex);
  },
  convertModelRowIndexToView: function (modelIndex) {
    return this.rowIndexMapper.convertModelIndexToView(modelIndex);
  },
  convertModelColumnIndexToView: function (modelIndex) {
    return this.columnIndexMapper.convertModelIndexToView(modelIndex);
  },
  isColumnViewIndexSelected: function (index) {
    return this.columnSelectionModel.isViewIndexSelected(index);
  },
  isRowViewIndexSelected: function (index) {
    return this.rowSelectionModel.isViewIndexSelected(index);
  }
};
phantasus.Util.extend(phantasus.Project, phantasus.Events);

phantasus.QNorm = function () {

};
/**
 * Performs quantile normalization.
 */
phantasus.QNorm.execute = function (data) {
  var rows = data.getRowCount();
  var cols = data.getColumnCount();
  var i, j, ind;
  var dimat;
  var row_mean = new Float32Array(rows);
  var ranks = new Float32Array(rows);
  /* # sort original columns */
  dimat = phantasus.QNorm.get_di_matrix(data);
  for (j = 0; j < cols; j++) {
    dimat[j].sort(function (s1, s2) {
      if (s1.data < s2.data) {
        return -1;
      }
      if (s1.data > s2.data) {
        return 1;
      }
      return 0;
    });

  }
  /* # calculate means */
  for (i = 0; i < rows; i++) {
    var sum = 0.0;
    var numNonMissing = 0;
    for (j = 0; j < cols; j++) {
      var f = dimat[j][i].data;
      if (!isNaN(f)) {
        sum += f;
        numNonMissing++;
      }
    }
    row_mean[i] = sum / numNonMissing;
  }

  /* # unsort mean columns */
  for (j = 0; j < cols; j++) {
    phantasus.QNorm.get_ranks(ranks, dimat[j], rows);
    for (i = 0; i < rows; i++) {
      ind = dimat[j][i].rank;
      if (ranks[i] - Math.floor(ranks[i]) > 0.4) {
        data.setValue(ind, j, 0.5 * (row_mean[Math.floor(ranks[i]) - 1] + row_mean[Math.floor(ranks[i])]));
      } else {
        data.setValue(ind, j, row_mean[Math.floor(ranks[i]) - 1]);
      }
    }
  }
};

/**
 * ************************************************************************
 * * * dataitem **get_di_matrix(var *data, var rows, var cols) * * given
 * data form a matrix of dataitems, each element of * matrix holds datavalue
 * and original index so that * normalized data values can be resorted to
 * the original order *
 * ************************************************************************
 */

phantasus.QNorm.get_di_matrix = function (data) {
  var i, j;
  var rows = data.getRowCount();
  var cols = data.getColumnCount();
  var dimat = [];
  for (j = 0; j < cols; j++) {
    dimat.push([]);
    for (i = 0; i < rows; i++) {
      dimat[j][i] = {};
      dimat[j][i].data = data.getValue(i, j);
      dimat[j][i].rank = i;
    }
  }
  return dimat;
};

/**
 * ************************************************************************
 * * * var *get_ranks(dataitem *x,var n) * * getParameterValue ranks in
 * the same manner as R does. Assume that *x is * already sorted *
 * ************************************************************************
 */

phantasus.QNorm.get_ranks = function (rank, x, n) {
  var i, j, k;
  i = 0;
  while (i < n) {
    j = i;
    while ((j < n - 1) && (x[j].data == x[j + 1].data)) {
      j++;
    }
    if (i != j) {
      for (k = i; k <= j; k++) {
        rank[k] = (i + j + 2) / 2.0;
      }
    } else {
      rank[i] = i + 1;
    }
    i = j + 1;
  }
};


phantasus.SelectionModel = function (project, isColumns) {
  this.viewIndices = new phantasus.Set();
  this.project = project;
  this.isColumns = isColumns;
};
phantasus.SelectionModel.prototype = {
  setViewIndices: function (indices, notify) {
    this.viewIndices = indices;
    if (notify) {
      this.trigger('selectionChanged');
    }
  },
  isViewIndexSelected: function (index) {
    return this.viewIndices.has(index);
  },
  clear: function () {
    this.viewIndices = new phantasus.Set();
  },
  /**
   *
   * @returns {phantasus.Set}
   */
  getViewIndices: function () {
    return this.viewIndices;
  },
  count: function () {
    return this.viewIndices.size();
  },
  toModelIndices: function () {
    var project = this.project;
    var f = this.isColumns ? project.convertViewColumnIndexToModel
      : project.convertViewRowIndexToModel;
    f = _.bind(f, project);
    var modelIndices = [];
    this.viewIndices.forEach(function (index) {
      var m = f(index);
      modelIndices.push(m);
    });
    return modelIndices;
  },
  save: function () {
    this.modelIndices = this.toModelIndices();
  },
  restore: function () {
    var project = this.project;
    this.viewIndices = new phantasus.Set();
    var f = this.isColumns ? project.convertModelColumnIndexToView
      : project.convertModelRowIndexToView;
    f = _.bind(f, project);
    for (var i = 0, length = this.modelIndices.length; i < length; i++) {
      var index = f(this.modelIndices[i]);
      if (index !== -1) {
        this.viewIndices.add(index);
      }
    }
  },
};
phantasus.Util.extend(phantasus.SelectionModel, phantasus.Events);

phantasus.SlicedDatasetView = function (dataset, rowIndices, columnIndices) {
  phantasus.DatasetAdapter.call(this, dataset);
  if (rowIndices == null) {
    rowIndices = null;
  }
  if (columnIndices == null) {
    columnIndices = null;
  }
  this.rowIndices = rowIndices;
  this.columnIndices = columnIndices;
  //phantasus.DatasetUtil.toESSessionPromise(this);
};
phantasus.SlicedDatasetView.prototype = {
  setESSession: function (session) {
    //// console.log("phantasus.SlicedDatasetView.prototype.setESSession ::", this, session);
    this.dataset.setESSession(session);
  },
  getESSession: function () {
    //// console.log("phantasus.SlicedDatasetView.prototype.getESSession ::", this);
    return this.dataset.getESSession();
  },
  setESVariable: function (variable) {
    this.dataset.setESVariable(variable);
  },
  getESVariable: function () {
    return this.dataset.getESVariable();
  },
  getRowCount: function () {
    return this.rowIndices !== null ? this.rowIndices.length : this.dataset
      .getRowCount();
  },
  getColumnCount: function () {
    return this.columnIndices !== null ? this.columnIndices.length
      : this.dataset.getColumnCount();
  },
  getValue: function (i, j, seriesIndex) {
    return this.dataset.getValue(
      this.rowIndices !== null ? this.rowIndices[i] : i,
      this.columnIndices !== null ? this.columnIndices[j] : j,
      seriesIndex);
  },
  setValue: function (i, j, value, seriesIndex) {
    this.dataset.setValue(
      this.rowIndices !== null ? this.rowIndices[i] : i,
      this.columnIndices !== null ? this.columnIndices[j] : j, value,
      seriesIndex);
  },
  getRowMetadata: function () {
    return this.rowIndices !== null ? new phantasus.MetadataModelItemView(
      this.dataset.getRowMetadata(), this.rowIndices) : this.dataset
      .getRowMetadata();
  },
  getColumnMetadata: function () {
    return this.columnIndices !== null ? new phantasus.MetadataModelItemView(
      this.dataset.getColumnMetadata(), this.columnIndices)
      : this.dataset.getColumnMetadata();
  },
  toString: function () {
    return this.getName();
  }
};
phantasus.Util.extend(phantasus.SlicedDatasetView, phantasus.DatasetAdapter);

phantasus.SlicedVector = function (v, indices) {
  phantasus.VectorAdapter.call(this, v);
  this.indices = indices;
};
phantasus.SlicedVector.prototype = {
  setValue: function (i, value) {
    this.v.setValue(this.indices[i], value);
  },
  getValue: function (i) {
    return this.v.getValue(this.indices[i]);
  },
  size: function () {
    return this.indices.length;
  }
};
phantasus.Util.extend(phantasus.SlicedVector, phantasus.VectorAdapter);

phantasus.AbstractSortKey = function (name, columns) {
  this.name = name;
  this.columns = columns;
};

phantasus.AbstractSortKey.prototype = {
  lockOrder: 0,
  columns: true,
  preservesDendrogram: false,
  unlockable: true,
  /**
   * Indicates whether this key is sorting rows or columns.
   * @return {Boolean}
   */
  isColumns: function () {
    return this.columns;
  },
  /**
   * Sets whether this key is columns (true) or rows (false).
   * @param columns {Boolean}
   */
  setColumns: function (columns) {
    this.columns = columns;
  },
  isPreservesDendrogram: function () {
    return this.preservesDendrogram;
  },
  setPreservesDendrogram: function (preservesDendrogram) {
    this.preservesDendrogram = preservesDendrogram;
  },
  getLockOrder: function () {
    return this.lockOrder;
  },
  /**
   * When lock order is set, indicates whether lock order can be unlocked. For example, bring matches to top sort key can not be unlocked.
   */
  isUnlockable: function () {
    return this.unlockable;
  },
  setUnlockable: function (unlockable) {
    this.unlockable = unlockable;
  },
  /**
   * Sets the sort key lock order. One is locked to beginning of sort keys, two is locked to end of sort keys. Zero clears lock order.
   * Dendrogram sort key is locked to end. Selection on top sort key is locked to beginning.
   * @param lockOrder {Number}
   */
  setLockOrder: function (lockOrder) {
    this.lockOrder = lockOrder;
  },
  setSortOrder: function (sortOrder) {
    this.sortOrder = sortOrder;
  },
  getSortOrder: function () {
    return this.sortOrder;
  },
  init: function () {

  }
};
phantasus.MatchesOnTopSortKey = function (project, modelIndices, name, columns) {
  phantasus.AbstractSortKey.call(this, name, columns);
  var highlightedModelIndices = {};
  var p = project;
  var viewIndices = [];
  for (var i = 0, j = modelIndices.length, length = modelIndices.length; i < length; i++, j--) {
    highlightedModelIndices[modelIndices[i]] = -1; // tie
    viewIndices.push(i);
  }
  this.comparator = function (i1, i2) {
    var a = highlightedModelIndices[i1];
    if (a === undefined) {
      a = 0;
    }
    var b = highlightedModelIndices[i2];
    if (b === undefined) {
      b = 0;
    }
    return (a === b ? 0 : (a < b ? -1 : 1));
  };
  this.indices = viewIndices;
};
phantasus.MatchesOnTopSortKey.prototype = {
  toString: function () {
    return this.name;
  },
  getSortOrder: function () {
    return 2;
  },
  getComparator: function () {
    return this.comparator;
  },
  getValue: function (i) {
    return i;
  }
};
phantasus.Util.extend(phantasus.MatchesOnTopSortKey, phantasus.AbstractSortKey);

phantasus.SortKey = function (field, sortOrder, columns) {
  phantasus.AbstractSortKey.call(this, field, columns);
  if (typeof sortOrder === 'string') {
    sortOrder = phantasus.SortKey.SortOrder[sortOrder.toUpperCase()];
    if (sortOrder === undefined) {
      sortOrder = 0;
    }
  }
  this.v = null;
  this.c = null;
  this.setSortOrder(sortOrder);
};

phantasus.SortKey.prototype = {
  toString: function () {
    return this.name;
  },
  init: function (dataset, visibleModelIndices) {
    this.v = dataset.getRowMetadata().getByName(this.name);
    if (!this.v) {
      this.v = {};
      this.v.getValue = function () {
        return 0;
      };
      this.c = this.sortOrder === phantasus.SortKey.SortOrder.ASCENDING ? phantasus.SortKey.ASCENDING_COMPARATOR
        : phantasus.SortKey.DESCENDING_COMPARATOR;
    } else {
      var dataType = phantasus.VectorUtil.getDataType(this.v);
      if (dataType === 'number') {
        this.c = this.sortOrder === phantasus.SortKey.SortOrder.ASCENDING ? phantasus.SortKey.NUMBER_ASCENDING_COMPARATOR
          : phantasus.SortKey.NUMBER_DESCENDING_COMPARATOR;
      } else if (dataType === '[number]') {
        var summary = this.v.getProperties().get(
          phantasus.VectorKeys.ARRAY_SUMMARY_FUNCTION)
          || phantasus.SortKey.ARRAY_MAX_SUMMARY_FUNCTION;
        this.c = this.sortOrder === phantasus.SortKey.SortOrder.ASCENDING ? phantasus.SortKey
            .ARRAY_ASCENDING_COMPARATOR(summary)
          : phantasus.SortKey.ARRAY_DESCENDING_COMPARATOR(summary);
      } else {
        this.c = this.sortOrder === phantasus.SortKey.SortOrder.ASCENDING ? phantasus.SortKey.ASCENDING_COMPARATOR
          : phantasus.SortKey.DESCENDING_COMPARATOR;
      }
      if (this.customComparator != null) {
        var oldC = this.c;
        var customComparator = this.customComparator;
        if (this.sortOrder === phantasus.SortKey.SortOrder.ASCENDING) {
          this.c = function (a, b) {
            var val = customComparator(a, b);
            return val === 0 ? oldC(a, b) : val;
          };
        } else {
          this.c = function (a, b) {
            var val = customComparator(b, a);
            return val === 0 ? oldC(a, b) : val;
          };
        }
      }
    }

    if (this.sortOrder === phantasus.SortKey.SortOrder.TOP_N) {
      var pairs = [];
      var missingIndices = [];
      for (var i = 0, nrows = visibleModelIndices.length; i < nrows; i++) {
        var index = visibleModelIndices[i];
        var value = this.v.getValue(index);
        if (!isNaN(value)) {
          pairs.push({
            index: index,
            value: value
          });
        } else {
          missingIndices.push(index);
        }
      }
      // sort values in descending order
      var c = this.c;
      this.c = phantasus.SortKey.NUMBER_ASCENDING_COMPARATOR;
      pairs
        .sort(function (pair1, pair2) {
          return c(pair1.value, pair2.value);
        });

      var modelIndexToValue = [];
      var nInGroup = Math.min(pairs.length, 10);
      var counter = 0;
      var topIndex = 0;

      var half = Math.floor(pairs.length / 2);
      var topPairs = pairs.slice(0, half);
      var bottomPairs = pairs.slice(half);
      var bottomIndex = bottomPairs.length - 1;
      var ntop = topPairs.length;
      var npairs = pairs.length;
      while (counter < npairs) {
        for (var i = 0; i < nInGroup && topIndex < ntop; i++, topIndex++, counter++) {
          modelIndexToValue[topPairs[topIndex].index] = counter;
        }
        var indexCounterPairs = [];
        for (var i = 0; i < nInGroup && bottomIndex >= 0; i++, bottomIndex--, counter++) {
          indexCounterPairs.push([
            bottomPairs[bottomIndex].index,
            counter]);
        }
        for (var i = indexCounterPairs.length - 1, j = 0; i >= 0; i--, j++) {
          var item_i = indexCounterPairs[i];
          var item_j = indexCounterPairs[j];
          modelIndexToValue[item_i[0]] = item_j[1];
        }

      }

      // add on missing
      for (var i = 0, length = missingIndices.length; i < length; i++, counter++) {
        modelIndexToValue[missingIndices[i]] = counter;
      }
      this.modelIndexToValue = modelIndexToValue;

    }
    else {
      delete this.modelIndexToValue;
    }
  },
  getComparator: function () {
    return this.c;
  },
  getValue: function (i) {
    return this.modelIndexToValue ? this.modelIndexToValue[i] : this.v.getValue(i);
  }
};
phantasus.Util.extend(phantasus.SortKey, phantasus.AbstractSortKey);
/**
 * @param modelIndices
 *            Selected rows or columns
 * @param isColumnSort -
 *            sort columns by selected rows.
 */
phantasus.SortByValuesKey = function (modelIndices, sortOrder, isColumnSort) {
  phantasus.AbstractSortKey.call(this, 'values', isColumnSort);
  this.bothCount = 10;
  this.modelIndices = modelIndices;
  this.sortOrder = sortOrder;
  this.setSortOrder(sortOrder);

};
phantasus.SortByValuesKey.prototype = {
  toString: function () {
    return this.name;
  },
  init: function (dataset, visibleModelIndices) {
    // isColumnSort-sort columns by selected rows
    // dataset is transposed if !isColumnSort
    this.dataset = phantasus.DatasetUtil.slicedView(dataset, null,
      this.modelIndices);
    this.rowView = new phantasus.DatasetRowView(this.dataset);
    this.summaryFunction = this.modelIndices.length > 1 ? phantasus.Median
      : function (row) {
      return row.getValue(0);
    };
    if (this.sortOrder === phantasus.SortKey.SortOrder.TOP_N) {
      var pairs = [];
      var missingIndices = [];
      for (var i = 0, nrows = visibleModelIndices.length; i < nrows; i++) {
        var index = visibleModelIndices[i];
        var value = this.summaryFunction(this.rowView.setIndex(index));
        if (!isNaN(value)) {
          pairs.push({
            index: index,
            value: value
          });
        } else {
          missingIndices.push(index);
        }
      }
      // sort values in descending order
      pairs
        .sort(function (a, b) {
          return (a.value < b.value ? 1
            : (a.value === b.value ? 0 : -1));
        });

      var modelIndexToValue = [];
      var nInGroup = Math.min(pairs.length, this.bothCount);
      var counter = 0;
      var topIndex = 0;

      var half = Math.floor(pairs.length / 2);
      var topPairs = pairs.slice(0, half);
      var bottomPairs = pairs.slice(half);
      var bottomIndex = bottomPairs.length - 1;
      var ntop = topPairs.length;
      var npairs = pairs.length;
      while (counter < npairs) {
        for (var i = 0; i < nInGroup && topIndex < ntop; i++, topIndex++, counter++) {
          modelIndexToValue[topPairs[topIndex].index] = counter;
        }
        var indexCounterPairs = [];
        for (var i = 0; i < nInGroup && bottomIndex >= 0; i++, bottomIndex--, counter++) {
          indexCounterPairs.push([
            bottomPairs[bottomIndex].index,
            counter]);
        }
        for (var i = indexCounterPairs.length - 1, j = 0; i >= 0; i--, j++) {
          var item_i = indexCounterPairs[i];
          var item_j = indexCounterPairs[j];
          modelIndexToValue[item_i[0]] = item_j[1];
        }

      }

      // add on missing
      for (var i = 0, length = missingIndices.length; i < length; i++, counter++) {
        modelIndexToValue[missingIndices[i]] = counter;
      }
      this.modelIndexToValue = modelIndexToValue;

    } else {
      delete this.modelIndexToValue;
    }
  },
  getComparator: function () {
    return this.c;
  },
  getValue: function (i) {
    return this.modelIndexToValue ? this.modelIndexToValue[i] : this
      .summaryFunction(this.rowView.setIndex(i));
  },
  setSortOrder: function (sortOrder) {
    if (typeof sortOrder === 'string') {
      sortOrder = phantasus.SortKey.SortOrder[sortOrder.toUpperCase()];
    }
    this.sortOrder = sortOrder;
    if (this.sortOrder === phantasus.SortKey.SortOrder.ASCENDING) {
      this.c = phantasus.SortKey.ELEMENT_ASCENDING_COMPARATOR;
    } else if (this.sortOrder === phantasus.SortKey.SortOrder.DESCENDING) {
      this.c = phantasus.SortKey.ELEMENT_DESCENDING_COMPARATOR;
    } else {
      this.c = phantasus.SortKey.NUMBER_ASCENDING_COMPARATOR;
    }

  }
};
phantasus.Util.extend(phantasus.SortByValuesKey, phantasus.AbstractSortKey);

/**
 * @param modelIndices
 *            Array of model indices
 * @param nvisible
 *            The number of visible indices at the time this sort key was
 *            created. Used by dendrogram to determine if dendrogram should be
 *            shown.
 * @param name
 *            This sort key name
 * @param columns Whether column sort
 */
phantasus.SpecifiedModelSortOrder = function (modelIndices, nvisible, name, columns) {
  phantasus.AbstractSortKey.call(this, name, columns);
  this.nvisible = nvisible;
  var modelIndexToValue = [];
  for (var i = 0, length = modelIndices.length; i < length; i++) {
    modelIndexToValue[modelIndices[i]] = i;
  }
  this.modelIndices = modelIndices;
  this.modelIndexToValue = modelIndexToValue;
  this.c = phantasus.SortKey.NUMBER_ASCENDING_COMPARATOR;
};
phantasus.SpecifiedModelSortOrder.prototype = {
  toString: function () {
    return this.name;
  },
  getComparator: function (a, b) {
    return this.c;
  },
  getValue: function (i) {
    return this.modelIndexToValue[i];
  },
  setSortOrder: function (sortOrder) {
    this.sortOrder = sortOrder;
    this.c = this.sortOrder === phantasus.SortKey.SortOrder.ASCENDING ? phantasus.SortKey.NUMBER_ASCENDING_COMPARATOR
      : phantasus.SortKey.NUMBER_DESCENDING_COMPARATOR;
  }
};
phantasus.Util.extend(phantasus.SpecifiedModelSortOrder, phantasus.AbstractSortKey);

/**
 * Group by key
 *
 * @param values
 */
phantasus.SpecifiedGroupByKey = function (clusterIds, columns) {
  phantasus.AbstractSortKey.call(this, 'Dendrogram Cut', columns);
  this.clusterIds = clusterIds;
  this.c = function (a, b) {
    return (a === b ? 0 : // Values are equal
      (a < b ? -1 : // (-0.0, 0.0) or (!NaN, NaN)
        1));
  };
};
phantasus.SpecifiedGroupByKey.prototype = {
  toString: function () {
    return this.name;
  },
  getComparator: function (a, b) {
    return this.c;
  },
  getValue: function (i) {
    return this.clusterIds[i];
  }
};
phantasus.Util.extend(phantasus.SpecifiedGroupByKey, phantasus.AbstractSortKey);

phantasus.SortKey.SortOrder = {
  ASCENDING: 0,
  DESCENDING: 1,
  UNSORTED: 2,
  CUSTOM: 3,
  TOP_N: 4
};
/**
 * Comparator to sort ascending using lowercase string comparison
 */
phantasus.SortKey.ASCENDING_COMPARATOR = function (a, b) {
  // we want NaNs to end up at the bottom
  var aNaN = (a == null);
  var bNaN = (b == null);
  if (aNaN && bNaN) {
    return 0;
  }
  if (aNaN) {
    return 1;
  }
  if (bNaN) {
    return -1;
  }
  a = ('' + a).toLowerCase();
  b = ('' + b).toLowerCase();
  return (a === b ? 0 : (a < b ? -1 : 1));
};
/**
 * Comparator to sort descending using lowercase string comparison
 */
phantasus.SortKey.DESCENDING_COMPARATOR = function (a, b) {
  var aNaN = (a == null);
  var bNaN = (b == null);
  if (aNaN && bNaN) {
    return 0;
  }
  if (aNaN) {
    return 1;
  }
  if (bNaN) {
    return -1;
  }
  a = ('' + a).toLowerCase();
  b = ('' + b).toLowerCase();
  return (a === b ? 0 : (a < b ? 1 : -1));
};

phantasus.SortKey.NUMBER_ASCENDING_COMPARATOR = function (a, b) {
  // we want NaNs to end up at the bottom
  var aNaN = (a == null || isNaN(a));
  var bNaN = (b == null || isNaN(b));
  if (aNaN && bNaN) {
    return 0;
  }
  if (aNaN) {
    return 1;
  }
  if (bNaN) {
    return -1;
  }
  return (a === b ? 0 : (a < b ? -1 : 1));
};

phantasus.SortKey.NUMBER_DESCENDING_COMPARATOR = function (a, b) {
  var aNaN = (a == null || isNaN(a));
  var bNaN = (b == null || isNaN(b));
  if (aNaN && bNaN) {
    return 0;
  }
  //noinspection JSConstructorReturnsPrimitive
  if (aNaN) {
    return 1;
  }
  if (bNaN) {
    return -1;
  }
  return (a === b ? 0 : (a < b ? 1 : -1));
};

phantasus.SortKey.STRING_ASCENDING_COMPARATOR = function (a, b) {
  a = (a == null || a.toLowerCase === undefined) ? null : a.toLowerCase();
  b = (b == null || b.toLowerCase === undefined) ? null : b.toLowerCase();
  return (a === b ? 0 : (a < b ? -1 : 1));
};
phantasus.SortKey.STRING_DESCENDING_COMPARATOR = function (a, b) {
  a = (a == null || a.toLowerCase === undefined) ? null : a.toLowerCase();
  b = (b == null || b.toLowerCase === undefined) ? null : b.toLowerCase();
  return (a === b ? 0 : (a < b ? 1 : -1));
};

phantasus.SortKey.ELEMENT_ASCENDING_COMPARATOR = function (obj1, obj2) {
  var a = +obj1;
  var b = +obj2;
  var aNaN = isNaN(a);
  var bNaN = isNaN(b);
  if (aNaN && bNaN) {
    return 0;
  }
  if (aNaN) {
    return 1;
  }
  if (bNaN) {
    return -1;
  }

  if (a === b) {
    if (obj1 != null && obj1.toObject && obj2 != null && obj2.toObject) {
      var a1 = obj1.toObject();
      var b1 = obj2.toObject();
      for (var name in a1) {
        a = a1[name];
        b = b1[name];

        var c = (a === b ? 0 : (a < b ? -1 : 1));
        if (c !== 0) {
          return c;
        }
      }
    }
  }
  return (a === b ? 0 : (a < b ? -1 : 1));
};

phantasus.SortKey.ELEMENT_DESCENDING_COMPARATOR = function (obj1, obj2) {
  // we want NaNs to end up at the bottom
  var a = +obj1;
  var b = +obj2;
  var aNaN = isNaN(a);
  var bNaN = isNaN(b);
  if (aNaN && bNaN) {
    return 0;
  }
  if (aNaN) {
    return 1;
  }
  if (bNaN) {
    return -1;
  }
  if (a === b) {
    if (obj1 != null && obj1.toObject && obj2 != null && obj2.toObject) {
      var a1 = obj1.toObject();
      var b1 = obj2.toObject();
      for (var name in a1) {
        a = a1[name];
        b = b1[name];
        var c = (a === b ? 0 : (a < b ? 1 : -1));
        if (c !== 0) {
          return c;
        }
      }
    }
  }
  return (a === b ? 0 : (a < b ? 1 : -1));
};
phantasus.SortKey.BOX_PLOT_SUMMARY_FUNCTION = function (array) {
  var box = array.box;
  if (box == null) {
    var v = phantasus.VectorUtil.arrayAsVector(array);
    box = phantasus
      .BoxPlotItem(this.indices != null ? new phantasus.SlicedVector(
        v, this.indices) : v);
    array.box = box;
  }

  return box.q3;
};

phantasus.SortKey.ARRAY_MAX_SUMMARY_FUNCTION = function (array) {
  var a = 0;
  if (array != null) {
    var aPosMax = -Number.MAX_VALUE;
    var aNegMax = Number.MAX_VALUE;
    for (var i = 0, length = array.length; i < length; i++) {
      var value = array[i];
      if (!isNaN(value)) {
        if (value >= 0) {
          aPosMax = value > aPosMax ? value : aPosMax;
        } else {
          aNegMax = value < aNegMax ? value : aNegMax;
        }
      }
    }

    if (aPosMax !== -Number.MAX_VALUE) {
      a = aPosMax;
    }
    if (aNegMax !== Number.MAX_VALUE) {
      a = Math.abs(aNegMax) > a ? aNegMax : a;
    }
  }
  return a;
};
phantasus.SortKey.ARRAY_ASCENDING_COMPARATOR = function (summary) {
  return function (a, b) {
    var aNaN = a == null;
    var bNaN = b == null;
    if (aNaN && bNaN) {
      return 0;
    }
    if (aNaN) {
      return 1;
    }
    if (bNaN) {
      return -1;
    }
    a = summary(a);
    b = summary(b);
    aNaN = isNaN(a);
    bNaN = isNaN(b);
    if (aNaN && bNaN) {
      return 0;
    }
    if (aNaN) {
      return 1;
    }
    if (bNaN) {
      return -1;
    }
    return (a === b ? 0 : (a < b ? -1 : 1));
  };
};

phantasus.SortKey.ARRAY_DESCENDING_COMPARATOR = function (summary) {
  return function (a, b) {
    var aNaN = a == null;
    var bNaN = b == null;
    if (aNaN && bNaN) {
      return 0;
    }
    if (aNaN) {
      return 1;
    }
    if (bNaN) {
      return -1;
    }
    a = summary(a);
    b = summary(b);
    aNaN = isNaN(a);
    bNaN = isNaN(b);
    if (aNaN && bNaN) {
      return 0;
    }
    if (aNaN) {
      return 1;
    }
    if (bNaN) {
      return -1;
    }
    return (a === b ? 0 : (a < b ? 1 : -1));
  };
};

phantasus.SortKey.reverseComparator = function (c) {
  return function (a, b) {
    return c(b, a);
  };
};
phantasus.SortKey.keepExistingSortKeys = function (newSortKeys, existingSortKeys) {
  for (var i = 0, length = existingSortKeys.length; i < length; i++) {
    var key = existingSortKeys[i];
    if (key.getLockOrder() > 0) {
      // 1 is beginning, 2 is end
      // don' add it 2x
      var existingIndex = -1;
      for (var j = 0; j < newSortKeys.length; j++) {
        if (newSortKeys[j] === key) {
          existingIndex = j;
          break;
        }
      }
      if (existingIndex !== -1) { // remove
        newSortKeys.splice(existingIndex, 1);
      }
      newSortKeys.splice(key.getLockOrder() === 1 ? 0 : newSortKeys.length, 0, key);
    }
  }
  return newSortKeys;
};

phantasus.SortKey.fromJSON = function (project, json) {
  var sortKeys = [];
  json.forEach(function (key) {
    var sortKey = null;
    if (key.type === 'annotation') {
      sortKey = new phantasus.SortKey(key.field, key.order, key.isColumns);
      if (key.customSortOrder != null) {

        var customSortOrderMap = new phantasus.Map();
        for (var i = 0, size = key.customSortOrder.length; i < size; i++) {
          customSortOrderMap.set(key.customSortOrder[i], i);
        }
        var comparator = function (a, b) {
          var v1 = customSortOrderMap.get(a);
          var v2 = customSortOrderMap.get(b);
          if (v1 === undefined && v2 === undefined) {
            return 0;
          }
          if (v1 === undefined) {
            v1 = Infinity;
          }
          if (v2 === undefined) {
            v2 = Infinity;
          }
          return (v1 < v2 ? -1 : 1);
        };
        sortKey.customComparator = comparator;
        if (key.preservesDendrogram) {
          sortKey.nvisible = key.customSortOrder.length;
        }
      }

    } else if (key.type === 'byValues') {
      sortKey = new phantasus.SortByValuesKey(key.modelIndices, key.order, key.isColumns);
    } else if (key.type === 'specified') {
      sortKey = new phantasus.SpecifiedModelSortOrder(key.modelIndices, key.nvisible, key.name, key.isColumns);
    } else if (key.type === 'matchesOnTop') {
      sortKey = new phantasus.MatchesOnTopSortKey(project, key.modelIndices, key.name, key.isColumns);
    } else {
      if (key.field != null) {
        sortKey = new phantasus.SortKey(key.field, key.order);
      } else {
        console.log('Unknown key: ' + key);
      }
    }
    if (sortKey != null) {
      if (key.preservesDendrogram != null) {
        sortKey.setPreservesDendrogram(key.preservesDendrogram);
      }
      if (key.lockOrder != null && key.lockOrder !== 0) {
        sortKey.setLockOrder(key.lockOrder);
        sortKey.setUnlockable(key.unlockable);
      }
      sortKeys.push(sortKey);
    }
    if (sortKey != null) {
      if (key.preservesDendrogram != null) {
        sortKey.setPreservesDendrogram(key.preservesDendrogram);
      }
      if (key.lockOrder != null && key.lockOrder !== 0) {
        sortKey.setLockOrder(key.lockOrder);
        sortKey.setUnlockable(key.unlockable);
      }
      sortKeys.push(sortKey);
    }
  });
  return sortKeys;
};

phantasus.SortKey.toJSON = function (sortKeys) {
  var json = [];
  sortKeys.forEach(function (key) {
    var sortKey = null;
    if (key instanceof phantasus.SortKey) {
      sortKey = {
        isColumns: key.isColumns(),
        order: key.getSortOrder(),
        type: 'annotation',
        field: '' + key
      };
    } else if (key instanceof phantasus.SortByValuesKey) {
      sortKey = {
        isColumns: key.isColumns(),
        order: key.getSortOrder(),
        type: 'byValues',
        modelIndices: key.modelIndices
      };
    } else if (key instanceof phantasus.SpecifiedModelSortOrder) {
      sortKey = {
        isColumns: key.isColumns(),
        order: key.getSortOrder(),
        type: 'specified',
        modelIndices: key.modelIndices,
        name: key.name,
        nvisible: key.nvisible
      };
    } else if (key instanceof phantasus.MatchesOnTopSortKey) {
      sortKey = {
        isColumns: key.isColumns(),
        order: key.getSortOrder(),
        type: 'matchesOnTop',
        modelIndices: key.modelIndices,
        name: key.name
      };
    }
    if (sortKey != null) {
      sortKey.preservesDendrogram = key.isPreservesDendrogram();
      if (key.getLockOrder && key.getLockOrder() !== 0) {
        sortKey.lockOrder = key.getLockOrder();
        sortKey.unlockable = key.isUnlockable ? key.isUnlockable() : false;
      }
      json.push(sortKey);
    } else {
      console.log('Unknown sort key type');
    }
  });
  return json;
};

phantasus.SymmetricProjectListener = function (project, vscroll, hscroll) {
  var ignoreEvent = false;
  var rowGroupBy;
  var columnGroupBy;
  var rowFilter;
  var columnFilter;
  var rowSortOrder;
  var columnSortOrder;
  var columnSelection;
  var rowSelection;
  var vscrollFunction;
  var hscrollFunction;
  project.on(phantasus.Project.Events.ROW_GROUP_BY_CHANGED, rowGroupBy = function () {
    if (ignoreEvent) {
      return;
    }
    ignoreEvent = true;
    project.setGroupColumns(project.getGroupRows(), true);
    ignoreEvent = false;
  });
  project.on(phantasus.Project.Events.COLUMN_GROUP_BY_CHANGED, columnGroupBy = function () {
    if (ignoreEvent) {
      return;
    }
    ignoreEvent = true;
    project.setGroupRows(project.getGroupColumns(), true);
    ignoreEvent = false;
  });
  project.on(phantasus.Project.Events.ROW_FILTER_CHANGED, rowFilter = function () {
    if (ignoreEvent) {
      return;
    }
    ignoreEvent = true;
    project.setColumnFilter(project.getRowFilter(), true);
    ignoreEvent = false;
  });
  project.on(phantasus.Project.Events.COLUMN_FILTER_CHANGED, columnFilter = function () {
    if (ignoreEvent) {
      return;
    }
    ignoreEvent = true;
    project.setRowFilter(project.getColumnFilter(), true);
    ignoreEvent = false;
  });
  project.on(phantasus.Project.Events.ROW_SORT_ORDER_CHANGED, rowSortOrder = function () {
    if (ignoreEvent) {
      return;
    }
    ignoreEvent = true;
    project.setColumnSortKeys(project.getRowSortKeys(), true);
    ignoreEvent = false;
  });
  project.on(phantasus.Project.Events.COLUMN_SORT_ORDER_CHANGED, columnSortOrder = function () {
    if (ignoreEvent) {
      return;
    }
    ignoreEvent = true;
    project.setRowSortKeys(project.getColumnSortKeys(), true);
    ignoreEvent = false;
  });
  project.getColumnSelectionModel().on('selectionChanged', columnSelection = function () {
    if (ignoreEvent) {
      return;
    }
    ignoreEvent = true;
    project.getRowSelectionModel().setViewIndices(project.getColumnSelectionModel().getViewIndices(), true);
    ignoreEvent = false;
  });
  project.getRowSelectionModel().on('selectionChanged', rowSelection = function () {
    if (ignoreEvent) {
      return;
    }
    ignoreEvent = true;
    project.getColumnSelectionModel().setViewIndices(project.getRowSelectionModel().getViewIndices(), true);
    ignoreEvent = false;
  });
  vscroll.on('scroll', vscrollFunction = function () {
    if (ignoreEvent) {
      return;
    }
    ignoreEvent = true;
    var f = vscroll.getMaxValue() === 0 ? 0 : vscroll.getValue() / vscroll.getMaxValue();
    hscroll.setValue(f * hscroll.getMaxValue(), true);
    ignoreEvent = false;
  });
  hscroll.on('scroll', hscrollFunction = function () {
    if (ignoreEvent) {
      return;
    }
    ignoreEvent = true;
    var f = hscroll.getMaxValue() === 0 ? 0 : hscroll.getValue() / hscroll.getMaxValue();
    vscroll.setValue(f * vscroll.getMaxValue(), true);
    ignoreEvent = false;
  });

  this.dispose = function () {
    project.off(phantasus.Project.Events.ROW_GROUP_BY_CHANGED, rowGroupBy);
    project.off(phantasus.Project.Events.COLUMN_GROUP_BY_CHANGED, columnGroupBy);
    project.off(phantasus.Project.Events.ROW_FILTER_CHANGED, rowFilter);
    project.off(phantasus.Project.Events.COLUMN_FILTER_CHANGED, columnFilter);
    project.off(phantasus.Project.Events.ROW_SORT_ORDER_CHANGED, rowSortOrder);
    project.off(phantasus.Project.Events.COLUMN_SORT_ORDER_CHANGED, columnSortOrder);
    project.getColumnSelectionModel().off('selectionChanged', columnSelection);
    project.getRowSelectionModel().off('selectionChanged', rowSelection);
    vscroll.off('scroll', vscrollFunction);
    hscroll.off('scroll', hscrollFunction);
  };
};




phantasus.TransposedDatasetView = function (dataset) {
  phantasus.DatasetAdapter.call(this, dataset);
};
phantasus.TransposedDatasetView.prototype = {
  getRowCount: function () {
    return this.dataset.getColumnCount();
  },
  getColumnCount: function () {
    return this.dataset.getRowCount();
  },
  getValue: function (i, j, seriesIndex) {
    return this.dataset.getValue(j, i, seriesIndex);
  },
  setValue: function (i, j, value, seriesIndex) {
    this.dataset.setValue(j, i, value, seriesIndex);
  },
  getRowMetadata: function () {
    return this.dataset.getColumnMetadata();
  },
  getColumnMetadata: function () {
    return this.dataset.getRowMetadata();
  }
};
phantasus.Util.extend(phantasus.TransposedDatasetView, phantasus.DatasetAdapter);

/**
 * Provides percentile computation.
 * <p>
 * There are several commonly used methods for estimating percentiles (a.k.a.
 * quantiles) based on sample data. For large samples, the different methods
 * agree closely, but when sample sizes are small, different methods will give
 * significantly different results. The algorithm implemented here works as
 * follows:
 * <ol>
 * <li>Let <code>n</code> be the length of the (sorted) array and
 * <code>0 < p <= 100</code> be the desired percentile.</li>
 * <li>If <code> n = 1 </code> return the unique array element (regardless of
 * the value of <code>p</code>); otherwise</li>
 * <li>Compute the estimated percentile position
 * <code> pos = p * (n + 1) / 100</code> and the difference, <code>d</code>
 * between <code>pos</code> and <code>floor(pos)</code> (i.e. the fractional
 * part of <code>pos</code>). If <code>pos >= n</code> return the largest
 * element in the array; otherwise</li>
 * <li>Let <code>lower</code> be the element in position
 * <code>floor(pos)</code> in the array and let <code>upper</code> be the
 * next element in the array. Return <code>lower + d * (upper - lower)</code></li>
 * </ol>
 *
 * @param p Percentile between 0 and 100
 */
phantasus.Percentile = function (vector, p, isSorted) {
  return phantasus.ArrayPercentile(phantasus.RemoveNaN(vector), p, isSorted);
};
phantasus.Percentile.toString = function () {
  return 'Percentile';
};
/**
 * @private
 * @ignore
 */
phantasus.RemoveNaN = function (values) {
  var array = [];
  for (var i = 0, size = values.size(); i < size; i++) {
    var value = values.getValue(i);
    if (!isNaN(value)) {
      array.push(value);
    }
  }
  return array;
};
phantasus.Median = function (vector) {
  return phantasus.ArrayPercentile(phantasus.RemoveNaN(vector), 50, false);
};
phantasus.Median.toString = function () {
  return 'Median';
};
/**
 * @ignore
 */
phantasus.ArrayPercentile = function (values, p, isSorted) {

  if (!isSorted) {
    values.sort(function (a, b) {
      return (a < b ? -1 : (a === b ? 0 : 1));
    });
  }
  return d3.quantile(values, p / 100);
};
/**
 * @ignore
 */
phantasus.MaxPercentiles = function (percentiles) {
  var f = function (vector) {
    var values = [];
    for (var i = 0, size = vector.size(); i < size; i++) {
      var value = vector.getValue(i);
      if (!isNaN(value)) {
        values.push(value);
      }
    }
    if (values.length === 0) {
      return NaN;
    }
    values.sort(function (a, b) {
      return (a < b ? -1 : (a === b ? 0 : 1));
    });
    var max = 0;
    for (var i = 0; i < percentiles.length; i++) {
      var p = phantasus.ArrayPercentile(values, percentiles[i], true);
      if (Math.abs(p) > Math.abs(max)) {
        max = p;
      }
    }
    return max;
  };
  f.toString = function () {
    var s = ['Maximum of '];
    for (var i = 0, length = percentiles.length; i < length; i++) {
      if (i > 0 && length > 2) {
        s.push(', ');
      }
      if (i === length - 1) {
        s.push(length == 2 ? ' and ' : 'and ');
      }
      s.push(percentiles[i]);
    }
    s.push(' percentiles');
    return s.join('');
  };
  return f;
};

phantasus.CountIf = function (vector, criteria) {
  if (!/[<>=!]/.test(criteria)) {
    criteria = '=="' + criteria + '"';
  }
  var matches = 0;
  for (var i = 0, size = vector.size(); i < size; i++) {
    var value = vector.getValue(i);
    if (eval(value + criteria)) {
      matches++;
    }
  }
  return matches;

};
phantasus.Mean = function (vector) {
  var sum = 0;
  var count = 0;
  for (var i = 0, length = vector.size(); i < length; i++) {
    var val = vector.getValue(i);
    if (!isNaN(val)) {
      sum += val;
      count++;
    }
  }
  return count === 0 ? NaN : sum / count;
};
phantasus.Mean.toString = function () {
  return 'Mean';
};
phantasus.Sum = function (vector) {
  var sum = 0;
  var found = false;
  for (var i = 0, length = vector.size(); i < length; i++) {
    var val = vector.getValue(i);
    if (!isNaN(val)) {
      found = true;
      sum += val;
    }
  }
  return !found ? NaN : sum;
};
phantasus.Sum.toString = function () {
  return 'Sum';
};
phantasus.CountNonNaN = function (vector) {
  var count = 0;
  for (var i = 0, length = vector.size(); i < length; i++) {
    var val = vector.getValue(i);
    if (!isNaN(val)) {
      count++;
    }
  }
  return count;
};
phantasus.CountNonNaN.toString = function () {
  return 'Count non-NaN';
};

phantasus.Max = function (vector) {
  var max = -Number.MAX_VALUE;
  var found = false;
  for (var i = 0, length = vector.size(); i < length; i++) {
    var val = vector.getValue(i);
    if (!isNaN(val)) {
      found = true;
      max = Math.max(max, val);
    }
  }
  return !found ? NaN : max;
};
phantasus.Max.toString = function () {
  return 'Max';
};
phantasus.Min = function (vector) {
  var min = Number.MAX_VALUE;
  var found = false;
  for (var i = 0, length = vector.size(); i < length; i++) {
    var val = vector.getValue(i);
    if (!isNaN(val)) {
      found = true;
      min = Math.min(min, val);
    }
  }
  return !found ? NaN : min;
};
phantasus.Min.toString = function () {
  return 'Min';
};
phantasus.Variance = function (list, mean) {
  if (mean == undefined) {
    mean = phantasus.Mean(list);
  }
  var sum = 0;
  var n = 0;
  for (var j = 0, size = list.size(); j < size; j++) {
    var x = list.getValue(j);
    if (!isNaN(x)) {
      var diff = x - mean;
      diff = diff * diff;
      sum += diff;
      n++;
    }
  }
  if (n <= 1) {
    return NaN;
  }
  n = n - 1;
  if (n < 1) {
    n = 1;
  }
  var variance = sum / n;
  return variance;
};
phantasus.Variance.toString = function () {
  return 'Variance';
};

phantasus.StandardDeviation = function (list, mean) {
  return Math.sqrt(phantasus.Variance(list, mean));
};
phantasus.StandardDeviation.toString = function () {
  return 'Standard deviation';
};

var LOG_10 = Math.log(10);
phantasus.Log10 = function (x) {
  return x <= 0 ? 0 : Math.log(x) / LOG_10;
};
var LOG_2 = Math.log(2);
phantasus.Log2 = function (x) {
  return x <= 0 ? 0 : Math.log(x) / LOG_2;
};

/**
 * Computes the False Discovery Rate using the BH procedure.
 *
 * @param nominalPValues
 *            Array of nominal p-values.
 */
phantasus.FDR_BH = function (nominalPValues) {
  var size = nominalPValues.length;
  var fdr = [];
  var pValueIndices = phantasus.Util.indexSort(nominalPValues, true);
  var ranks = phantasus.Util.rankIndexArray(pValueIndices);

  // check for ties
  for (var i = pValueIndices.length - 1; i > 0; i--) {
    var bigPValue = nominalPValues[pValueIndices[i]];
    var smallPValue = nominalPValues[pValueIndices[i - 1]];
    if (bigPValue == smallPValue) {
      ranks[pValueIndices[i - 1]] = ranks[pValueIndices[i]];
    }
  }
  for (var i = 0; i < size; i++) {
    var rank = ranks[i];
    var p = nominalPValues[i];
    fdr[i] = (p * size) / rank;
  }

  // ensure fdr is monotonically decreasing
  var pIndices = phantasus.Util.indexSort(nominalPValues, false);
  for (var i = 0; i < pIndices.length - 1; i++) {
    var highIndex = pIndices[i];
    var lowIndex = pIndices[i + 1];
    fdr[lowIndex] = Math.min(fdr[lowIndex], fdr[highIndex]);
  }
  for (var i = 0; i < size; i++) {
    fdr[i] = Math.min(fdr[i], 1);
  }
  return fdr;
};

phantasus.FDR_BH.tString = function () {
  return 'FDR(BH)';
};

phantasus.MAD = function (list, median) {
  if (median == null) {
    median = phantasus.Percentile(list, 50);
  }
  var temp = [];
  for (var j = 0, size = list.size(); j < size; j++) {
    var value = list.getValue(j);
    if (!isNaN(value)) {
      temp.push(Math.abs(value - median));
    }
  }
  var r = phantasus.Percentile(new phantasus.Vector('', temp.length)
    .setArray(temp), 50);
  return 1.4826 * r;
};
phantasus.MAD.toString = function () {
  return 'Median absolute deviation';
};
phantasus.CV = function (list) {
  var mean = phantasus.Mean(list);
  var stdev = Math.sqrt(phantasus.Variance(list, mean));
  return stdev / mean;
};
phantasus.CV.toString = function () {
  return 'Coefficient of variation';
};

phantasus.BoxPlotItem = function (list) {
  var values = phantasus.RemoveNaN(list);
  values.sort(function (a, b) {
    return (a === b ? 0 : (a < b ? -1 : 1));
  });
  if (values.length === 0) {
    return {
      median: NaN,
      q1: NaN,
      q3: NaN,
      lowerAdjacentValue: NaN,
      upperAdjacentValue: NaN
    };
  }
  return phantasus.BoxPlotArrayItem(values);
};

phantasus.BoxPlotArrayItem = function (values) {
  var median = phantasus.ArrayPercentile(values, 50, true);
  var q1 = phantasus.ArrayPercentile(values, 25, true);
  var q3 = phantasus.ArrayPercentile(values, 75, true);
  var w = 1.5;
  var upperAdjacentValue = -Number.MAX_VALUE;
  var lowerAdjacentValue = Number.MAX_VALUE;
  // The upper adjacent value (UAV) is the largest observation that is
  // less than or equal to
  // the upper inner fence (UIF), which is the third quartile plus
  // 1.5*IQR.
  //
  // The lower adjacent value (LAV) is the smallest observation that is
  // greater than or equal
  // to the lower inner fence (LIF), which is the first quartile minus
  // 1.5*IQR.
  var upperOutlier = q3 + w * (q3 - q1);
  var lowerOutlier = q1 - w * (q3 - q1);
  var sum = 0;
  for (var i = 0, length = values.length; i < length; i++) {
    var value = values[i];
    if (value <= upperOutlier) {
      upperAdjacentValue = Math.max(upperAdjacentValue, value);
    }
    if (value >= lowerOutlier) {
      lowerAdjacentValue = Math.min(lowerAdjacentValue, value);
    }
    sum += value;
    // if (value > upperOutlier) {
    // upperOutliers.add(new Outlier(i, j, value));
    // }
    // if (value < lowerOutlier) {
    // lowerOutliers.add(new Outlier(i, j, value));
    // }
  }
  var mean = sum / values.length;
  if (lowerAdjacentValue > q1) {
    lowerAdjacentValue = q1;
  }
  if (upperAdjacentValue < q3) {
    upperAdjacentValue = q3;
  }

  return {
    mean: mean,
    median: median,
    q1: q1, // Lower Quartile
    q3: q3, // Upper Quartile
    lowerAdjacentValue: lowerAdjacentValue, // Lower Whisker
    upperAdjacentValue: upperAdjacentValue
    // Upper Whisker
  };

};

phantasus.VectorColorModel = function () {
  this.vectorNameToColorMap = new phantasus.Map();
  this.vectorNameToColorScheme = new phantasus.Map();
  this.colors = phantasus.VectorColorModel.TWENTY_COLORS;
};

phantasus.VectorColorModel.YES_COLOR = '#d8b365';
phantasus.VectorColorModel.FEMALE = '#ff99ff';
phantasus.VectorColorModel.MALE = '#66ccff';

// tableau 20-same as d3 category20
phantasus.VectorColorModel.TWENTY_COLORS = [
  '#1f77b4', '#aec7e8', '#ff7f0e',
  '#ffbb78', '#2ca02c', '#98df8a', '#d62728', '#ff9896', '#9467bd',
  '#c5b0d5', '#8c564b', '#c49c94', '#e377c2', '#f7b6d2', '#7f7f7f',
  '#c7c7c7', '#bcbd22', '#dbdb8d', '#17becf', '#9edae5'];
phantasus.VectorColorModel.CATEGORY_20A = phantasus.VectorColorModel.TWENTY_COLORS;
phantasus.VectorColorModel.CATEGORY_20B = [
  '#393b79', '#5254a3', '#6b6ecf',
  '#9c9ede', '#637939', '#8ca252', '#b5cf6b', '#cedb9c', '#8c6d31',
  '#bd9e39', '#e7ba52', '#e7cb94', '#843c39', '#ad494a', '#d6616b',
  '#e7969c', '#7b4173', '#a55194', '#ce6dbd', '#de9ed6'];
phantasus.VectorColorModel.CATEGORY_20C = [
  '#3182bd', '#6baed6', '#9ecae1',
  '#c6dbef', '#e6550d', '#fd8d3c', '#fdae6b', '#fdd0a2', '#31a354',
  '#74c476', '#a1d99b', '#c7e9c0', '#756bb1', '#9e9ac8', '#bcbddc',
  '#dadaeb', '#636363', '#969696', '#bdbdbd', '#d9d9d9'];

phantasus.VectorColorModel.CATEGORY_ALL = [].concat(
  phantasus.VectorColorModel.CATEGORY_20A,
  phantasus.VectorColorModel.CATEGORY_20B,
  phantasus.VectorColorModel.CATEGORY_20C);

phantasus.VectorColorModel.TABLEAU10 = [
  '#1f77b4', '#ff7f0e', '#2ca02c',
  '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22',
  '#17becf'];
phantasus.VectorColorModel.STANDARD_COLORS = {
  'na': '#c0c0c0',
  'nan': '#c0c0c0',
  '': '#ffffff',
  'wt': '#ffffff',
  'n': '#ffffff',
  '0': '#ffffff',
  'y': phantasus.VectorColorModel.YES_COLOR,
  '1': phantasus.VectorColorModel.YES_COLOR,
  'male': phantasus.VectorColorModel.MALE,
  'm': phantasus.VectorColorModel.MALE,
  'female': phantasus.VectorColorModel.FEMALE,
  'f': phantasus.VectorColorModel.FEMALE,
  'kd': '#C675A8',
  'oe': '#56b4e9',
  'cp': '#FF9933',
  'pcl': '#003B4A',
  'trt_sh.cgs': '#C675A8',
  'trt_oe': '#56b4e9',
  'trt_cp': '#FF9933',
  'a375': '#1490C1',
  'a549': '#AAC8E9',
  'hcc515': '#1C9C2A',
  'hepg2': '#94DC89',
  'ht29': '#946DBE',
  'mcf7': '#C5B2D5',
  'pc3': '#38C697',
  'asc': '#FF8000',
  'cd34': '#FFBB75',
  'ha1e': '#FB4124',
  'neu': '#FF9A94',
  'npc': '#E57AC6',
  'cancer': '#1490C1',
  'immortalized normal': '#FF8000'
};
phantasus.VectorColorModel.getStandardColor = function (value) {
  if (value == null) {
    return '#ffffff';
  }
  var stringValue = value.toString().toLowerCase();
  return phantasus.VectorColorModel.STANDARD_COLORS[stringValue];

};
phantasus.VectorColorModel.getColorMapForNumber = function (length) {
  var colors;
  if (length < 3) {
    colors = colorbrewer.Set1[3];
  } else {
    colors = colorbrewer.Paired[length];
  }
  return colors ? colors : phantasus.VectorColorModel.TWENTY_COLORS;
};
phantasus.VectorColorModel.prototype = {
  toJSON: function (tracks) {
    var _this = this;
    var json = {};
    tracks.forEach(function (track) {
      if (track.getFullVector().getProperties().get(phantasus.VectorKeys.DISCRETE)) {
        var colorMap = _this.vectorNameToColorMap.get(track.getName());
        if (colorMap != null) {
          json[track.getName()] = colorMap;
        }
      } else {
        // colorScheme is instanceof phantasus.HeatMapColorScheme
        var colorScheme = _this.vectorNameToColorScheme.get(track.getName());
        if (colorScheme != null) {
          var colorSchemeJSON = phantasus.AbstractColorSupplier.toJSON(colorScheme.getCurrentColorSupplier());
          json[track.getName()] = colorSchemeJSON;
        }
      }
    });
    return json;
  },
  fromJSON: function (json) {
    for (var name in json) {
      var obj = json[name];
      if (obj.colors) {
        obj.scalingMode = 'fixed';
        this.vectorNameToColorScheme.set(name, phantasus.AbstractColorSupplier.fromJSON(obj));
      } else {
        this.vectorNameToColorMap.set(name, phantasus.Map.fromJSON(obj));
      }
    }
  },
  clear: function (vector) {
    this.vectorNameToColorMap.remove(vector.getName());
    this.vectorNameToColorScheme.remove(vector.getName());
  },
  copy: function () {
    var c = new phantasus.VectorColorModel();
    c.colors = this.colors.slice(0);
    this.vectorNameToColorMap.forEach(function (colorMap, name) {
      var newColorMap = new phantasus.Map();
      newColorMap.setAll(colorMap); // copy existing values
      c.vectorNameToColorMap.set(name, newColorMap);
    });
    this.vectorNameToColorScheme.forEach(function (colorScheme, name) {
      c.vectorNameToColorScheme.set(name, colorScheme
        .copy(new phantasus.Project(new phantasus.Dataset({
          name: '',
          rows: 1,
          columns: 1
        }))));
    });
    return c;
  },
  clearAll: function () {
    this.vectorNameToColorMap = new phantasus.Map();
    this.vectorNameToColorScheme = new phantasus.Map();
  },
  containsDiscreteColor: function (vector, value) {
    var metadataValueToColorMap = this.vectorNameToColorMap.get(vector
      .getName());
    if (metadataValueToColorMap === undefined) {
      return false;
    }
    var c = metadataValueToColorMap.get(value);
    return c != null;
  },
  setDiscreteColorMap: function (colors) {
    this.colors = colors;
  },
  getContinuousColorScheme: function (vector) {
    return this.vectorNameToColorScheme.get(vector.getName());
  },
  isContinuous: function (vector) {
    return this.vectorNameToColorScheme.has(vector.getName());
  },
  getDiscreteColorScheme: function (vector) {
    return this.vectorNameToColorMap.get(vector.getName());
  },
  createContinuousColorMap: function (vector) {
    var minMax = phantasus.VectorUtil.getMinMax(vector);
    var min = minMax.min;
    var max = minMax.max;
    var cs = new phantasus.HeatMapColorScheme(new phantasus.Project(
      new phantasus.Dataset({
        name: '',
        rows: 1,
        columns: 1
      })), {
      type: 'fixed',
      map: [
        {
          value: min,
          color: colorbrewer.Greens[3][0]
        }, {
          value: max,
          color: colorbrewer.Greens[3][2]
        }]
    });
    this.vectorNameToColorScheme.set(vector.getName(), cs);
    return cs;

  },
  _getColorForValue: function (value) {
    var color = phantasus.VectorColorModel.getStandardColor(value);
    if (color == null) { // try to reuse existing color map
      var existingMetadataValueToColorMap = this.vectorNameToColorMap
        .values();
      for (var i = 0, length = existingMetadataValueToColorMap.length; i < length; i++) {
        color = existingMetadataValueToColorMap[i].get(value);
        if (color !== undefined) {
          return color;
        }
      }
    }
    return color;
  },
  getContinuousMappedValue: function (vector, value) {
    var cs = this.vectorNameToColorScheme.get(vector.getName());
    if (cs === undefined) {
      cs = this.createContinuousColorMap(vector);
    }
    return cs.getColor(0, 0, value);
  },
  getMappedValue: function (vector, value) {
    //// console.log("getMappedValue", vector, value);
    var metadataValueToColorMap = this.vectorNameToColorMap.get(vector
      .getName());
    if (metadataValueToColorMap === undefined) {
      metadataValueToColorMap = new phantasus.Map();
      this.vectorNameToColorMap.set(vector.getName(),
        metadataValueToColorMap);
      // set all possible colors
      var values = phantasus.VectorUtil.getValues(vector);
      var ncolors = 0;
      var colors = null;
      if (values.length < 3) {
        colors = colorbrewer.Dark2[3];
      } else {
        colors = colorbrewer.Paired[values.length];
      }
      //// console.log("getMappedValue", colors);

      if (!colors) {
        if (values.length <= 20) {
          colors = d3.scale.category20().range();
        } else {
          colors = phantasus.VectorColorModel.CATEGORY_ALL;
        }
      }
      //// console.log("getMappedValue", colors);

      if (colors) {
        var ncolors = colors.length;
        for (var i = 0, nvalues = values.length; i < nvalues; i++) {
          var color = this._getColorForValue(values[i]);
          //// console.log(i, color, values[i], colors[i % ncolors]);
          if (color == null) {
            color = colors[i % ncolors];
          }
          metadataValueToColorMap.set(values[i], color);
        }
      } else {
        var _this = this;
        _.each(values, function (val) {
          _this.getMappedValue(vector, val);
        });
      }
      //// console.log(metadataValueToColorMap);
    }
    var color = metadataValueToColorMap.get(value);
    if (color == null) {
      color = this._getColorForValue(value);
      if (color == null) {
        var index = metadataValueToColorMap.size();
        color = this.colors[index % this.colors.length];
      }
      metadataValueToColorMap.set(value, color);
    }
    return color;
  },
  setMappedValue: function (vector, value, color) {
    var metadataValueToColorMap = this.vectorNameToColorMap.get(vector
      .getName());
    if (metadataValueToColorMap === undefined) {
      metadataValueToColorMap = new phantasus.Map();
      this.vectorNameToColorMap.set(vector.getName(),
        metadataValueToColorMap);
    }
    metadataValueToColorMap.set(value, color);
  }
};

phantasus.VectorFontModel = function () {
  this.vectorNameToMappedValue = new phantasus.Map();
  this.fonts = phantasus.VectorFontModel.FONTS;

};

phantasus.VectorFontModel.FONTS = [{weight: 400}, {weight: 700}, {weight: 900}];
// 400 (normal), 700 (bold), 900 (bolder)

phantasus.VectorFontModel.prototype = {
  toJSON: function (tracks) {
    var _this = this;
    var json = {};
    tracks.forEach(function (track) {
      if (track.isRenderAs(phantasus.VectorTrack.RENDER.TEXT) && track.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_FONT)) {
        var map = _this.vectorNameToMappedValue.get(track.getName());
        if (map != null) {
          json[track.getName()] = map;
        }
      }
    });
    return json;
  },
  fromJSON: function (json) {
    for (var name in json) {
      var obj = json[name];
      this.vectorNameToMappedValue.set(name, phantasus.Map.fromJSON(obj));
    }
  },
  clear: function (vector) {
    this.vectorNameToMappedValue.remove(vector.getName());
  },
  copy: function () {
    var c = new phantasus.VectorFontModel();
    c.fonts = this.fonts.slice(0);
    this.vectorNameToMappedValue.forEach(function (fontMap, name) {
      var newFontMap = new phantasus.Map();
      newFontMap.setAll(fontMap); // copy existing values
      c.vectorNameToMappedValue.set(name, newFontMap);
    });
    return c;
  },
  clearAll: function () {
    this.vectorNameToMappedValue = new phantasus.Map();
  },
  _getFontForValue: function (value) {
    if (value == null) {
      return phantasus.VectorFontModel.FONTS[0];
    }
    // try to reuse existing map
    var existingMetadataValueToFontMap = this.vectorNameToMappedValue
      .values();
    for (var i = 0, length = existingMetadataValueToFontMap.length; i < length; i++) {
      var font = existingMetadataValueToFontMap[i].get(value);
      if (font !== undefined) {
        return font;
      }
    }
  },
  getMap: function (name) {
    return this.vectorNameToMappedValue.get(name);
  },
  getMappedValue: function (vector, value) {
    var metadataValueToFontMap = this.vectorNameToMappedValue.get(vector
      .getName());
    if (metadataValueToFontMap === undefined) {
      metadataValueToFontMap = new phantasus.Map();
      this.vectorNameToMappedValue.set(vector.getName(),
        metadataValueToFontMap);
      // set all possible values
      var values = phantasus.VectorUtil.getValues(vector);
      for (var i = 0, nvalues = values.length; i < nvalues; i++) {
        var font = this._getFontForValue(values[i]);
        if (font == null) {
          font = this.fonts[0]; // default is normal
        }
        metadataValueToFontMap.set(values[i], font);
      }
    }
    var font = metadataValueToFontMap.get(value);
    if (font == null) {
      font = this._getFontForValue(value);
      if (font == null) {
        font = this.fonts[0]; // default is normal
      }
      metadataValueToFontMap.set(value, font);
    }
    return font;
  },
  setMappedValue: function (vector, value, font) {
    var metadataValueToFontMap = this.vectorNameToMappedValue.get(vector
      .getName());
    if (metadataValueToFontMap === undefined) {
      metadataValueToFontMap = new phantasus.Map();
      this.vectorNameToMappedValue.set(vector.getName(),
        metadataValueToFontMap);
    }
    metadataValueToFontMap.set(value, font);
  }
};

/**
 * An interface for an ordered collection of values.
 *
 * @interface phantasus.VectorInterface
 */

/**
 * Returns the value at the specified index.
 *
 * @function
 * @name phantasus.VectorInterface#getValue
 * @param index the index
 * @return the value
 */

/**
 * Gets the key-value pairs associated with this vector.
 *
 * @function
 * @name phantasus.VectorInterface#getProperties
 * @return {phantasus.Map}
 */

/**
 * Returns the number of elements in this vector.
 *
 * @function
 * @name phantasus.VectorInterface#size
 * @return {number} the size.
 */

/**
 * Returns the name of this vector.
 *
 * @function
 * @name phantasus.VectorInterface#getName
 * @return {string} the name
 */




phantasus.VectorKeys = {};
/** [string] of field names in array */
phantasus.VectorKeys.FIELDS = 'phantasus.fields';
phantasus.VectorKeys.VALUE_TO_INDICES = 'phantasus.valueToIndices';
/** [int] of visible field indices in phantasus.VectorKeys.FIELDS */
phantasus.VectorKeys.VISIBLE_FIELDS = 'phantasus.visibleFields';
phantasus.VectorKeys.DATA_TYPE = 'phantasus.dataType';
/** Function to map an array to a single value for sorting */
phantasus.VectorKeys.ARRAY_SUMMARY_FUNCTION = 'phantasus.arraySummaryFunct';
/** Key for object (e.g. box plot) that summarizes data values */
phantasus.VectorKeys.HEADER_SUMMARY = 'phantasus.headerSummary';
/** Key indicating to show header summary */
phantasus.VectorKeys.SHOW_HEADER_SUMMARY = 'phantasus.showHeaderSummary';

phantasus.VectorKeys.TITLE = 'phantasus.title';
/** Function to compute vector value */
phantasus.VectorKeys.FUNCTION = 'phantasus.funct';

/** Indicates that vector values are dynamically computed based on selection */
phantasus.VectorKeys.SELECTION = 'phantasus.selection';

/** Whether to recompute a function when creating a new heat map (true or false) */
phantasus.VectorKeys.RECOMPUTE_FUNCTION_NEW_HEAT_MAP = 'phantasus.recompute.funct.new.heat.map';

/** Whether to recompute a function when filter is updated (true or false)  */
phantasus.VectorKeys.RECOMPUTE_FUNCTION_FILTER = 'phantasus.recompute.funct.filter';

/** Boolean, whether to recompute a function when heat map selection changes */
phantasus.VectorKeys.RECOMPUTE_FUNCTION_SELECTION = 'phantasus.recompute.funct.selection';

/**Number format spec/function */
phantasus.VectorKeys.FORMATTER = 'phantasus.formatter';

/* Indicates that a "fake" vector to show row/column number */
phantasus.VectorKeys.IS_INDEX = 'phantasus.isIndex';

/** Whether vector values should be treated discretely or continuously */
phantasus.VectorKeys.DISCRETE = 'phantasus.discrete';

phantasus.VectorKeys.COPY_IGNORE = new phantasus.Set();
phantasus.VectorKeys.COPY_IGNORE.add(phantasus.VectorKeys.HEADER_SUMMARY);
phantasus.VectorKeys.COPY_IGNORE.add(phantasus.VectorKeys.DATA_TYPE);
phantasus.VectorKeys.COPY_IGNORE.add(phantasus.VectorKeys.VALUE_TO_INDICES);

phantasus.VectorKeys.JSON_WHITELIST = new phantasus.Set();
phantasus.VectorKeys.JSON_WHITELIST.add(phantasus.VectorKeys.FIELDS);
phantasus.VectorKeys.JSON_WHITELIST.add(phantasus.VectorKeys.FORMATTER);
phantasus.VectorKeys.JSON_WHITELIST.add(phantasus.VectorKeys.DATA_TYPE);
phantasus.VectorKeys.JSON_WHITELIST.add(phantasus.VectorKeys.TITLE);

phantasus.VectorShapeModel = function () {
  this.shapes = phantasus.VectorShapeModel.SHAPES;
  this.vectorNameToMappedValue = new phantasus.Map();
};

phantasus.VectorShapeModel.SHAPES = [
  'circle', 'square', 'plus', 'x',
  'asterisk', 'diamond', 'triangle-up', 'triangle-down', 'triangle-left',
  'triangle-right', 'circle-minus'];

phantasus.VectorShapeModel.FILLED_SHAPES = [
  'circle', 'square', 'diamond', 'triangle-up', 'triangle-down', 'triangle-left',
  'triangle-right'];

phantasus.VectorShapeModel.prototype = {
  toJSON: function (tracks) {
    var _this = this;
    var json = {};
    tracks.forEach(function (track) {
      if (track.isRenderAs(phantasus.VectorTrack.RENDER.SHAPE)) {
        var map = _this.vectorNameToMappedValue.get(track.getName());
        if (map != null) {
          json[track.getName()] = map;
        }
      }
    });
    return json;
  },
  fromJSON: function (json) {
    for (var name in json) {
      var obj = json[name];
      this.vectorNameToMappedValue.set(name, phantasus.Map.fromJSON(obj));
    }
  },
  clear: function (vector) {
    this.vectorNameToMappedValue.remove(vector.getName());
  },
  copy: function () {
    var c = new phantasus.VectorShapeModel();
    c.shapes = this.shapes.slice(0);
    this.vectorNameToMappedValue.forEach(function (shapeMap, name) {
      var newShapeMap = new phantasus.Map();
      newShapeMap.setAll(shapeMap); // copy existing values
      c.vectorNameToMappedValue.set(name, newShapeMap);
    });

    return c;
  },
  clearAll: function () {
    this.vectorNameToMappedValue = new phantasus.Map();
  },
  _getShapeForValue: function (value) {
    if (value == null) {
      return 'none';
    }

    // try to reuse existing map
    var existingMetadataValueToShapeMap = this.vectorNameToMappedValue
      .values();
    for (var i = 0, length = existingMetadataValueToShapeMap.length; i < length; i++) {
      var shape = existingMetadataValueToShapeMap[i].get(value);
      if (shape !== undefined) {
        return shape;
      }
    }

  },
  getMap: function (name) {
    return this.vectorNameToMappedValue.get(name);
  },
  getMappedValue: function (vector, value) {
    var metadataValueToShapeMap = this.vectorNameToMappedValue.get(vector
      .getName());
    if (metadataValueToShapeMap === undefined) {
      metadataValueToShapeMap = new phantasus.Map();
      this.vectorNameToMappedValue.set(vector.getName(),
        metadataValueToShapeMap);
      // set all possible shapes
      var values = phantasus.VectorUtil.getValues(vector);
      for (var i = 0, nvalues = values.length; i < nvalues; i++) {
        var shape = this._getShapeForValue(values[i]);
        if (shape == null) {
          shape = this.shapes[i % this.shapes.length];
        }
        metadataValueToShapeMap.set(values[i], shape);
      }
    }
    var shape = metadataValueToShapeMap.get(value);
    if (shape == null) {
      shape = this._getShapeForValue(value);
      if (shape == null) {
        var index = metadataValueToShapeMap.size();
        shape = this.shapes[index % this.shapes.length];
      }
      metadataValueToShapeMap.set(value, shape);
    }
    return shape;
  },
  setMappedValue: function (vector, value, shape) {
    var metadataValueToShapeMap = this.vectorNameToMappedValue.get(vector
      .getName());
    if (metadataValueToShapeMap === undefined) {
      metadataValueToShapeMap = new phantasus.Map();
      this.vectorNameToMappedValue.set(vector.getName(),
        metadataValueToShapeMap);
    }
    metadataValueToShapeMap.set(value, shape);
  }
};

phantasus.VectorUtil = function () {
};

phantasus.VectorUtil.jsonToFunction = function (vector, key) {
  var f = vector.getProperties().get(key);
  if (typeof f === 'object') {
    // TODO encode other functions
    var binSize = f.binSize;
    var min = f.domain[0];
    var max = f.domain[1];
    var numberOfBins = Math.ceil((max - min) / binSize);
    var percent = f.percent;
    var cumulative = f.cumulative;
    var histogramFunction = function (view, selectedDataset, columnIndex) {
      var total = 0;
      var binNumberToOccurences = new Uint32Array(numberOfBins);
      for (var i = 0, nrows = selectedDataset.getRowCount(); i < nrows; i++) {
        var value = selectedDataset.getValue(i, columnIndex);
        if (!isNaN(value)) {
          if (value >= min && value <= max) {
            var bin = Math.floor(((value - min) / binSize));
            if (bin < 0) {
              bin = 0;
            } else if (bin >= numberOfBins) {
              bin = numberOfBins - 1;
            }
            binNumberToOccurences[bin]++;
          }
          total++;
        }
      }
      if (cumulative) {
        for (var i = numberOfBins - 2; i >= 0; i--) {
          binNumberToOccurences[i] += binNumberToOccurences[i + 1];
        }
      }
      if (percent) {
        var percents = new Float32Array(numberOfBins);
        for (var i = 0; i < numberOfBins; i++) {
          percents[i] = 100 * (binNumberToOccurences[i] / total);
        }
        return percents;
      }
      return binNumberToOccurences;
    };
    vector.getProperties().set(key, histogramFunction);
    var jsonSpec = f;
    f = histogramFunction;
    f.toJSON = function () {
      return jsonSpec;
    };
  }
  return f;
};
phantasus.VectorUtil.createValueToIndexMap = function (vector, splitArrayValues) {
  var map = new phantasus.Map();
  var isArray = splitArrayValues && phantasus.VectorUtil.getDataType(vector)[0] === '[';
  for (var j = 0, size = vector.size(); j < size; j++) {
    var val = vector.getValue(j);
    if (isArray) {
      if (val != null) {
        for (var k = 0; k < val.length; k++) {
          map.set(val[k], j);
        }
      }
    } else {
      map.set(val, j);
    }
  }
  return map;
};

phantasus.VectorUtil.createValueToIndicesMap = function (vector, splitArrayValues) {
  if (!vector) {
    throw 'vector is null';
  }
  var isArray = splitArrayValues && phantasus.VectorUtil.getDataType(vector)[0] === '[';
  var map = new phantasus.Map();
  for (var j = 0, size = vector.size(); j < size; j++) {
    var val = vector.getValue(j);
    if (isArray) {
      if (val != null) {
        for (var k = 0; k < val.length; k++) {
          var list = map.get(val[k]);
          if (list === undefined) {
            list = [];
            map.set(val[k], list);
          }
          list.push(j);
        }
      }
    } else {
      var list = map.get(val);
      if (list === undefined) {
        list = [];
        map.set(val, list);
      }
      list.push(j);
    }
  }
  return map;
};

phantasus.VectorUtil.createValueToCountMap = function (vector) {
  if (!vector) {
    throw 'vector is null';
  }
  var map = new phantasus.Map();
  var dataType = phantasus.VectorUtil.getDataType(vector);
  var isArray = dataType[0] === '[';
  for (var j = 0, size = vector.size(); j < size; j++) {
    var val = vector.getValue(j);
    if (val != null) {
      if (isArray) {
        for (var k = 0; k < val.length; k++) {
          var count = map.get(val[k]) || 0;
          map.set(val[k], count + 1);
        }
      } else {
        var count = map.get(val) || 0;
        map.set(val, count + 1);
      }
    }
  }
  return map;
};

phantasus.VectorUtil.createValuesToIndicesMap = function (vectors) {
  var map = new phantasus.Map();
  var nvectors = vectors.length;
  if (vectors[0] == null) {
    throw 'no vectors found';
  }
  for (var i = 0, nitems = vectors[0].size(); i < nitems; i++) {
    var array = [];
    for (var j = 0; j < nvectors; j++) {
      var vector = vectors[j];
      var val = vector.getValue(i);
      array.push(val);
    }
    var key = new phantasus.Identifier(array);
    var list = map.get(key);
    if (list === undefined) {
      list = [];
      map.set(key, list);
    }
    list.push(i);
  }
  return map;
};
phantasus.VectorUtil.createValuesToIndexMap = function (vectors) {
  var map = new phantasus.Map();
  var nvectors = vectors.length;
  if (vectors[0] == null) {
    throw 'no vectors found';
  }
  for (var i = 0, nitems = vectors[0].size(); i < nitems; i++) {
    var array = [];
    for (var j = 0; j < nvectors; j++) {
      var vector = vectors[j];
      var val = vector.getValue(i);
      array.push(val);
    }
    var key = new phantasus.Identifier(array);
    map.set(key, i);
  }
  return map;
};

phantasus.VectorUtil.createValuesToCountMap = function (vectors) {
  var map = new phantasus.Map();
  var nvectors = vectors.length;
  if (vectors[0] == null) {
    throw 'no vectors found';
  }
  for (var i = 0, nitems = vectors[0].size(); i < nitems; i++) {
    var array = [];
    for (var j = 0; j < nvectors; j++) {
      var vector = vectors[j];
      var val = vector.getValue(i);
      array.push(val);
    }
    var key = new phantasus.Identifier(array);
    var count = map.get(key) || 0;
    map.set(key, count + 1);
  }
  return map;
};

/**
 *
 * @param vector
 * @param excludeNull
 * @returns A sorted array of unique values contained in the vector. Note that array values are
 * not split.
 */
phantasus.VectorUtil.getValues = function (vector, excludeNull) {
  var set = new phantasus.Set();
  for (var j = 0, size = vector.size(); j < size; j++) {
    var val = vector.getValue(j);
    if (excludeNull && val == null) {
      continue;
    }
    set.add(val);
  }
  var array = set.values();
  array.sort(phantasus.SortKey.ASCENDING_COMPARATOR);
  return array;
};

phantasus.VectorUtil.getSet = function (vector, splitArrayValues) {
  var set = new phantasus.Set();
  var isArray = splitArrayValues && phantasus.VectorUtil.getDataType(vector)[0] === '[';
  for (var j = 0, size = vector.size(); j < size; j++) {
    var value = vector.getValue(j);
    if (isArray) {
      if (value != null) {
        for (var k = 0, nvalues = value.length; k < nvalues; k++) {
          set.add(value[k]);
        }
      }
    } else {
      set.add(value);
    }

  }
  return set;
};
phantasus.VectorUtil.maybeConvertToStringArray = function (vector, delim) {
  var newValues = [];
  var regex = new RegExp(delim);
  var found = false;

  for (var i = 0, nrows = vector.size(); i < nrows; i++) {
    var s = vector.getValue(i);
    if (s != null) {
      if (!s.split) {
        return false;
      }
      var tokens = s.split(regex);
      newValues.push(tokens);
      if (!found && tokens.length > 1) {
        found = true;
      }
    }

  }
  if (found) {
    for (var i = 0, nrows = newValues.length; i < nrows; i++) {
      vector.setValue(i, newValues[i]);
    }
    vector.getProperties().set(phantasus.VectorKeys.DATA_TYPE, '[string]');
  }

  return found;
};

phantasus.VectorUtil.maybeConvertStringToNumber = function (vector) {
  var newValues = [];
  var found = false;
  for (var i = 0, nrows = vector.size(); i < nrows; i++) {
    var s = vector.getValue(i);
    if (s != null && s !== '' && s !== 'NA' && s !== 'NaN') {
      if (!$.isNumeric(s)) {
        return false;
      } else {
        found = true;
      }
    }
    newValues.push(parseFloat(s));
  }
  if (!found) {
    return false;
  }
  for (var i = 0, nrows = newValues.length; i < nrows; i++) {
    vector.setValue(i, newValues[i]);
  }
  vector.getProperties().set(phantasus.VectorKeys.DATA_TYPE, 'number');
  return true;
};
phantasus.VectorUtil.containsMoreThanOneValue = function (vector) {
  return phantasus.VectorUtil.containsMoreThanNValues(vector, 1);
};
phantasus.VectorUtil.containsMoreThanNValues = function (vector, n) {
  var s = new phantasus.Set();
  for (var j = 0, size = vector.size(); j < size; j++) {
    var val = vector.getValue(j);
    s.add(val);
    if (s.size() > n) {
      return true;
    }
  }
  return false;
};

phantasus.VectorUtil.createSpanMap = function (vector) {
  var previous = vector.getValue(0);
  // find 1st row with different value
  var startIndexToEndIndex = new phantasus.Map();
  var start = 0;
  for (var i = 1, nrows = vector.size(); i < nrows; i++) {
    var val = vector.getValue(i);
    if (previous !== val) {
      previous = val;
      // start inclusive, end exclusive
      startIndexToEndIndex.set(start, i);
      start = i;
    }
  }
  startIndexToEndIndex.set(start, vector.size());
  return startIndexToEndIndex;
};
phantasus.VectorUtil.toArray = function (vector) {
  var array = [];
  for (var i = 0, length = vector.size(); i < length; i++) {
    var val = vector.getValue(i);
    array.push(val);
  }
  return array;
};

phantasus.VectorUtil.arrayAsVector = function (array, name) {
  var v = new phantasus.Vector(name, array.length);
  v.array = array;
  return v;
};
phantasus.VectorUtil.toString = function (vector) {
  var array = [];
  for (var i = 0, length = vector.size(); i < length; i++) {
    var val = vector.getValue(i);
    array.push(val);
  }
  return array.join(', ');
};

phantasus.VectorUtil.getDataType = function (vector) {
  var dataType = vector.getProperties().get(phantasus.VectorKeys.DATA_TYPE);
  if (dataType === undefined) {
    var firstNonNull = phantasus.VectorUtil.getFirstNonNull(vector);
    dataType = phantasus.Util.getDataType(firstNonNull);
    vector.getProperties().set(phantasus.VectorKeys.DATA_TYPE, dataType);
  }
  return dataType;

};

phantasus.VectorUtil.getMinMax = function (vector) {
  var min = Number.MAX_VALUE;
  var max = -Number.MAX_VALUE;
  var fields = vector.getProperties().get(phantasus.VectorKeys.FIELDS);
  var isArray = phantasus.VectorUtil.getDataType(vector)[0] === '[';
  if (fields != null) {
    var nvalues = fields.length;
    for (var i = 0, size = vector.size(); i < size; i++) {
      var array = vector.getValue(i);
      if (array) {
        for (var j = 0; j < nvalues; j++) {
          var value = array[j];
          if (!isNaN(value)) {
            min = value < min ? value : min;
            max = value > max ? value : max;
          }
        }
      }
    }
  } else if (isArray) {
    for (var i = 0, size = vector.size(); i < size; i++) {
      var array = vector.getValue(i);
      if (array != null) {
        for (var j = 0, nvalues = array.length; j < nvalues; j++) {
          var value = array[j];
          if (!isNaN(value)) {
            min = value < min ? value : min;
            max = value > max ? value : max;
          }
        }
      }
    }
  } else {
    for (var i = 0, size = vector.size(); i < size; i++) {
      var value = vector.getValue(i);
      if (!isNaN(value)) {
        min = value < min ? value : min;
        max = value > max ? value : max;
      }
    }
  }
  return {
    min: min,
    max: max
  };
}
;
phantasus.VectorUtil.getFirstNonNull = function (vector) {
  for (var i = 0, length = vector.size(); i < length; i++) {
    var val = vector.getValue(i);
    if (val != null) {
      return val;
    }
  }
  return null;
};
phantasus.VectorUtil.isNumber = function (vector) {
  return phantasus.VectorUtil.getDataType(vector) === 'number';
};

/**
 * An ordered collection of values.
 *
 * Creates a new vector with the given name and size.
 *
 * @param name
 *            the vector name
 * @param size
 *            the number of elements in this vector
 * @constructor
 */
phantasus.Vector = function (name, size) {
  this.array = [];
  phantasus.AbstractVector.call(this, name, size);
};
/**
 * @static
 */
phantasus.Vector.fromArray = function (name, array) {
  var v = new phantasus.Vector(name, array.length);
  v.array = array;
  return v;
};
phantasus.Vector.prototype = {
  /**
   * @ignore
   * @param value
   */
  push: function (value) {
    this.array.push(value);
  },
  /**
   * Sets the value at the specified index.
   *
   * @param index
   *            the index
   * @param value
   *            the value
   */
  setValue: function (index, value) {
    this.array[index] = value;
  },
  getValue: function (index) {
    return this.array[index];
  },
  /**
   * @ignore
   * @param name
   */
  setName: function (name) {
    this.name = name;
  },
  /**
   * @ignore
   * @param array
   * @returns {phantasus.Vector}
   */
  setArray: function (array) {
    this.array = array;
    return this;
  }
};
phantasus.Util.extend(phantasus.Vector, phantasus.AbstractVector);

/**
 *
 * @param pageOptions.el
 * @param pageOptions.tabManager
 * @constructor
 */
phantasus.LandingPage = function (pageOptions) {
  pageOptions = $.extend({}, {
    el: $('#vis')
  }, pageOptions);
  this.pageOptions = pageOptions;
  var _this = this;

  var $el = $('<div class="container" style="display: none;"></div>');
  this.$el = $el;
  var html = [];
  phantasus.Util.createPhantasusHeader().appendTo($el);
  html.push('<div data-name="help" class="pull-right"></div>');

  html.push('<h4>Open your own file</h4>');
  html.push('<div data-name="formRow" class="center-block"></div>');
  html.push('<div style="display: none;" data-name="preloadedDataset"><h4>Or select a preloaded' +
    ' dataset</h4></div>');
  html.push('</div>');
  var $html = $(html.join(''));

  $html.appendTo($el);
  new phantasus.HelpMenu().$el.appendTo($el.find('[data-name=help]'));
  var formBuilder = new phantasus.FormBuilder();
  formBuilder.append({
    name: 'file',
    showLabel: false,
    value: '',
    type: 'file',
    required: true,
    help: phantasus.DatasetUtil.DATASET_FILE_FORMATS
  });

  formBuilder.$form.appendTo($el.find('[data-name=formRow]'));
  this.formBuilder = formBuilder;
  this.$sampleDatasetsEl = $el.find('[data-name=preloadedDataset]');

  this.tabManager = new phantasus.TabManager({landingPage: this});
  this.tabManager.on('change rename add remove', function (e) {
    var title = _this.tabManager.getTabText(_this.tabManager.getActiveTabId());
    if (title == null || title === '') {
      title = 'phantasus';
    }
    document.title = title;
  });
  if (pageOptions.tabManager) {
    this.tabManager = pageOptions.tabManager;
  } else {
    this.tabManager = new phantasus.TabManager({landingPage: this});
    this.tabManager.on('change rename add remove', function (e) {
      var title = _this.tabManager.getTabText(_this.tabManager.getActiveTabId());
      if (title == null || title === '') {
        title = 'phantasus';
      }
      document.title = title;
    });

    this.tabManager.$nav.appendTo($(this.pageOptions.el));
    this.tabManager.$tabContent.appendTo($(this.pageOptions.el));
  }

}
;

phantasus.LandingPage.prototype = {
  open: function (openOptions) {
    this.dispose();
    var _this = this;

    var createGEOHeatMap = function(options)  {
      var req = ocpu.call('checkGPLs', { name : options.dataset.file }, function (session) {
        session.getMessages(function(success) {
          console.log('checkGPLs messages', '::', success);
        });
        session.getObject(function (filenames) {
          filenames = JSON.parse(filenames);
          // console.log("filenames", filenames, filenames.length);
          if (!filenames.length) {
            _this.show();
            throw new Error("Dataset" + " " + options.dataset.file + " does not exist");
          }
          if (filenames.length === 1) {
            new phantasus.HeatMap(options);
          }
          else {
            for (var j = 0; j < filenames.length; j++) {
              var specificOptions = options;
              specificOptions.dataset.file = filenames[j];

              new phantasus.HeatMap(specificOptions);
            }
          }
        })
      });
      req.fail(function () {
        throw new Error("Checking GPLs call to OpenCPU failed" + req.responseText);
      });
    };

    var createPreloadedHeatMap = function(options) {
      var req = ocpu.call('checkPreloadedNames', { name : options.dataset.file }, function(session) {
        session.getMessages(function(success) {
          console.log('checkPreloadedNames messages', success);
        });
        session.getObject(function(success) {
          var names = JSON.parse(success);
          // console.log(names);

          if (names.length === 0) {
            _this.show();
            throw new Error("Dataset" + " " + options.dataset.file + " does not exist");

          }

          for (var j = 0; j < names.length; j++) {
            var specificOptions = options;

            specificOptions.dataset.options.exactName = names[j];
            // console.log("specific", specificOptions);

            new phantasus.HeatMap(specificOptions);
          }

        })
      });
      req.fail(function () {
        throw new Error("Checking inside names call to OpenCPU failed" + req.responseText);
      });
    };

    var optionsArray = _.isArray(openOptions) ? openOptions : [openOptions];

    // console.log(optionsArray);
    for (var i = 0; i < optionsArray.length; i++) {
      var options = optionsArray[i];
      options.tabManager = _this.tabManager;
      options.focus = i === 0;
      options.standalone = true;
      options.landingPage = _this;

      if (options.dataset.options && options.dataset.options.isGEO) {
        createGEOHeatMap(options);
      } else if (options.dataset.options && options.dataset.options.preloaded) {
        createPreloadedHeatMap(options);
      }
      else {
        // console.log("before loading heatmap from landing_page", options);
        new phantasus.HeatMap(options);
      }
    }

  },
  dispose: function () {
    this.formBuilder.setValue('file', '');
    this.$el.hide();
    $(window)
      .off(
        'paste.phantasus drop.phantasus dragover.phantasus dragenter.phantasus');
    this.formBuilder.off('change');
  },
  show: function () {
    var _this = this;
    this.$el.show();

    this.formBuilder.on('change', function (e) {
      var value = e.value;
      if (value !== '' && value != null) {
        _this.openFile(value);
      }
    });

    $(window).on('beforeunload.phantasus', function () {
      if (_this.tabManager.getTabCount() > 0) {
        return 'Are you sure you want to close phantasus?';
      }
    });
    $(window).on('paste.phantasus', function (e) {
      var tagName = e.target.tagName;
      if (tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA') {
        return;
      }

      var text = e.originalEvent.clipboardData.getData('text/plain');
      if (text != null && text.length > 0) {
        e.preventDefault();
        e.stopPropagation();
        var url;
        if (text.indexOf('http') === 0) {
          url = text;
        } else {
          var blob = new Blob([text]);
          url = window.URL.createObjectURL(blob);
        }

        _this.openFile(url);
      }

    }).on('dragover.phantasus dragenter.phantasus', function (e) {
      e.preventDefault();
      e.stopPropagation();
    }).on(
      'drop.phantasus',
      function (e) {
        if (e.originalEvent.dataTransfer
          && e.originalEvent.dataTransfer.files.length) {
          e.preventDefault();
          e.stopPropagation();
          var files = e.originalEvent.dataTransfer.files;
          _this.openFile(files[0]);
        } else if (e.originalEvent.dataTransfer) {
          var url = e.originalEvent.dataTransfer.getData('URL');
          e.preventDefault();
          e.stopPropagation();
          _this.openFile(url);
        }
      });
    if (navigator.onLine && !this.sampleDatasets) {
      this.sampleDatasets = new phantasus.SampleDatasets({
        $el: this.$sampleDatasetsEl,
        show: true,
        callback: function (heatMapOptions) {
          _this.open(heatMapOptions);
        }
      });
    }
  },
  openFile: function (value) {
    var _this = this;
    var isGEO;
    var preloaded;
    if (value.name && (value.isGEO || value.preloaded)) {
      isGEO = value.isGEO;
      preloaded = value.preloaded;
      value = value.name;
    }

    var fileName = phantasus.Util.getFileName(value);
    if (fileName.toLowerCase().indexOf('.json') === fileName.length - 5) {
      phantasus.Util.getText(value).done(function (text) {
        _this.open(JSON.parse(text));
      }).fail(function (err) {
        phantasus.FormBuilder.showMessageModal({
          title: 'Error',
          message: 'Unable to load session'
        });
      });
    } else {
      var options = {
        dataset: {
          file: value,
          options: {
            interactive: true,
            isGEO: isGEO,
            preloaded: preloaded
          }
        }
      };

      phantasus.OpenDatasetTool.fileExtensionPrompt(fileName, function (readOptions) {
        // console.log("fileExtensionPrompt", readOptions);
        if (readOptions) {
          for (var key in readOptions) {
            options.dataset.options[key] = readOptions[key];
          }
        }
        _this.open(options);
      });
    }
  }
};

phantasus.SampleDatasets = function (options) {
  if (!options.openText) {
    options.openText = 'Open';
  }
  var _this = this;
  var $el = options.$el;
  this.callback = options.callback;
  $el.on('click', '[name=ccle]', function (e) { // button click
    var $this = $(this);
    var obj = {};
    $this.parents('tr').find('input:checked').each(function (i, c) {
      obj[$(c).data('type')] = true;
    });

    _this.openCCLE(obj);
    e.preventDefault();
  });

  $el.on('click', '[name=tcgaLink]', function (e) {
    e.preventDefault();
    var $this = $(this);
    var type = $this.data('disease-type');
    var obj = {};
    $this.parents('tr').find('input:checked').each(function (i, c) {
      obj[$(c).data('type')] = true;
    });
    var disease;
    for (var i = 0; i < _this.diseases.length; i++) {
      if (_this.diseases[i].type === type) {
        disease = _this.diseases[i];
        break;
      }
    }
    obj.type = type;
    obj.name = disease.name;
    _this.openTcga(obj);
  });

  $el.on(
    'click',
    '[data-toggle=dataTypeToggle]',
    function (e) {
      var $this = $(this);
      var $button = $this.parents('tr').find('button');
      var isDisabled = $this.parents('tr').find(
          'input:checked').length === 0;
      $button.prop('disabled', isDisabled);
    });

    fetch('https://software.broadinstitute.org/morpheus/preloaded-datasets/tcga/tcga_index.txt').then(function (response) {
      if (response.ok) {
        return response.text();
      }
    }).then(function (text) {
        var exampleHtml = [];
        exampleHtml.push('<table class="table table-condensed table-bordered">');
        exampleHtml.push('<thead><tr><th>Name</th><th>Gene' +
          ' Expression</th><th>Copy Number By Gene</th><th>Mutations</th><th>Gene' +
          ' Essentiality</th><th></th></tr></thead>');
        exampleHtml.push('<tbody>');
        exampleHtml.push('<tr>');
        exampleHtml
          .push('<td>Cancer Cell Line Encyclopedia (CCLE), Project Achilles</td>');
        exampleHtml
          .push('<td><input type="checkbox" style="margin-left:4px;" data-toggle="dataTypeToggle" data-type="mrna"> </td>');

        exampleHtml
          .push('<td><input type="checkbox" style="margin-left:4px;" data-toggle="dataTypeToggle" data-type="cn"> </td>');

        exampleHtml
          .push('<td><input type="checkbox" style="margin-left:4px;" data-toggle="dataTypeToggle" data-type="sig_genes"> </td>');

        exampleHtml
          .push('<td><input type="checkbox" style="margin-left:4px;" data-toggle="dataTypeToggle" data-type="ach"> </td>');

        exampleHtml
          .push('<td><button disabled type="button" class="btn btn-link" name="ccle">'
            + options.openText + '</button></td>');
        exampleHtml.push('</tr></tbody></table>');

      exampleHtml.push(
        '<div>TCGA data <a target="_blank" href="https://confluence.broadinstitute.org/display/GDAC/Dashboard-Stddata">(Broad GDAC 1/28/2016)</a></div><span>Please adhere to the' +
        ' <a target="_blank"' +
        ' href="http://cancergenome.nih.gov/abouttcga/policies/publicationguidelines">TCGA' +
        ' publication guidelines</a></u> when using TCGA data in your publications.</span>');

      exampleHtml.push('<div data-name="tcga"></div>');
        $(exampleHtml.join('')).appendTo($el);
        if (options.show) {
          $el.show();
        }
        var lines = text.split('\n');
        var diseases = [];
        for (var i = 0; i < lines.length; i++) {
          var line = lines[i];
          if (line === '') {
            continue;
          }
          var tokens = line.split('\t');
          var type = tokens[0];
          var dataTypes = tokens[1].split(',');
          var name = phantasus.TcgaUtil.DISEASE_STUDIES[type];
          var disease = {
            mrna: dataTypes
              .indexOf('mRNAseq_RSEM_normalized_log2.txt') !== -1,
            sig_genes: dataTypes.indexOf('sig_genes.txt') !== -1,
            gistic: dataTypes
              .indexOf('all_lesions.conf_99.txt') !== -1,
            sample_info: dataTypes.indexOf('All_CDEs.txt') !== -1,
            mutation: dataTypes
              .indexOf('mutations_merged.maf.txt') !== -1,
            rppa: dataTypes.indexOf('rppa.txt') !== -1,
            methylation: dataTypes
              .indexOf('meth.by_mean.data.txt') !== -1,
            name: name,
            type: type,
            dataTypes: dataTypes
          };
          if (disease.mrna || disease.gistic
            || disease.sig_genes || disease.rppa
            || disease.methylation) {
            diseases.push(disease);
          }
        }
        diseases.sort(function (a, b) {
          a = a.name.toLowerCase();
          b = b.name.toLowerCase();
          return (a === b ? 0 : (a < b ? -1 : 1));

        });
        var tcga = [];
        _this.diseases = diseases;

        tcga.push('<table class="table table-condensed table-bordered">');
        tcga.push('<thead><tr>');
        tcga.push('<th>Disease</th>');
        tcga.push('<th>Gene Expression</th>');
        tcga.push('<th>GISTIC Copy Number</th>');
        tcga.push('<th>Copy Number By Gene</th>');
        tcga.push('<th>Mutations</th>');
        tcga.push('<th>Proteomics</th>');
        tcga.push('<th>Methylation</th>');
        tcga.push('<th></th>');
        tcga.push('</tr></thead>');
        tcga.push('<tbody>');
        for (var i = 0; i < diseases.length; i++) {
          var disease = diseases[i];
          tcga.push('<tr>');

          tcga.push('<td>' + disease.name + '</td>');
          tcga.push('<td>');
          if (disease.mrna) {
            tcga
              .push('<input type="checkbox" style="margin-left:4px;" data-toggle="dataTypeToggle" data-type="mrna"> ');
          }
          tcga.push('</td>');

          tcga.push('<td>');
          if (disease.gistic) {
            tcga
              .push('<input type="checkbox" style="margin-left:4px;" data-toggle="dataTypeToggle" data-type="gistic"> ');
          }
          tcga.push('</td>');

          tcga.push('<td>');
          if (disease.gistic) {
            tcga
              .push('<input type="checkbox" style="margin-left:4px;" data-toggle="dataTypeToggle" data-type="gisticGene"> ');
          }
          tcga.push('</td>');

          tcga.push('<td>');
          if (disease.sig_genes) {
            tcga
              .push('<input type="checkbox" style="margin-left:4px;" data-toggle="dataTypeToggle" data-type="sig_genes"> ');
          }
          tcga.push('</td>');

          tcga.push('<td>');
          if (disease.rppa) {
            tcga
              .push('<input type="checkbox" style="margin-left:4px;" data-toggle="dataTypeToggle" data-type="rppa"> ');
          }
          tcga.push('</td>');
          tcga.push('<td>');
          if (disease.methylation) {
            tcga
              .push('<input type="checkbox" style="margin-left:4px;" data-toggle="dataTypeToggle" data-type="methylation"> ');
          }
          tcga.push('</td>');

          tcga
            .push('<td><button disabled type="button" class="btn btn-link" name="tcgaLink" data-disease-type="'
              + disease.type
              + '">'
              + options.openText
              + '</button></td>');
          tcga.push('</tr>');
        }
        tcga.push('</tbody>');
        tcga.push('</table>');
        $(tcga.join('')).appendTo($el.find('[data-name=tcga]'));
      }).catch(function (error) {
        console.log(error);
    });
};

phantasus.SampleDatasets.getTcgaDataset = function (options) {
  var baseUrl = 'https://software.broadinstitute.org/morpheus/preloaded-datasets/tcga/'
    + options.type + '/';
  var datasetOptions = {};
  if (options.mrna) {
    datasetOptions.mrna = baseUrl + 'mRNAseq_RSEM_normalized_log2.txt';
  }

  if (options.methylation) {
    datasetOptions.methylation = baseUrl + 'meth.by_mean.data.txt';
  }
  if (options.sig_genes) {
    datasetOptions.mutation = baseUrl + 'mutations_merged.maf.txt';
    datasetOptions.sigGenes = baseUrl + 'sig_genes.txt';
  }
  // datasetOptions.seg = baseUrl + 'snp.seg.txt';
  if (options.rppa) {
    datasetOptions.rppa = baseUrl + 'rppa.txt';
  }
  if (options.gistic) {
    datasetOptions.gistic = baseUrl + 'all_lesions.conf_99.txt';
  }
  if (options.gisticGene) {
    datasetOptions.gisticGene = baseUrl + 'all_data_by_genes.txt';
  }

  datasetOptions.mrnaClust = baseUrl + 'bestclus.txt';
  datasetOptions.columnAnnotations = [
    {
      file: baseUrl + 'All_CDEs.txt',
      datasetField: 'participant_id',
      fileField: 'patient_id', // e.g. tcga-5l-aat0
      transposed: false
    }];
  return phantasus.TcgaUtil.getDataset(datasetOptions);

};
phantasus.SampleDatasets.getCCLEDataset = function (options) {
  var datasets = [];
  if (options.sig_genes) {
    datasets.push(
      'https://software.broadinstitute.org/morpheus/preloaded-datasets/ccle2maf_081117.maf.txt');
  }
  if (options.cn) {
    datasets.push('https://software.broadinstitute.org/morpheus/preloaded-datasets/CCLE_copynumber_byGene_2013-12-03.gct');
  }

  if (options.mrna) {
    datasets.push('https://software.broadinstitute.org/morpheus/preloaded-datasets/CCLE_expression_081117.rpkm.gct');
  }
  if (options.ach) {
    datasets.push('https://software.broadinstitute.org/morpheus/preloaded-datasets/Achilles_v2.20.2_GeneSolutions.gct');
  }

  var d = $.Deferred();
  var datasetPromise = phantasus.DatasetUtil.readDatasetArray(datasets);
  datasetPromise.done(function (dataset) {
    var idVector = dataset.getColumnMetadata().get(0);
    var siteVector = dataset.getColumnMetadata().add('site');
    for (var j = 0, ncols = siteVector.size(); j < ncols; j++) {
      var id = idVector.getValue(j);
      var index = id.indexOf('_');
      if (index !== -1) {
        idVector.setValue(j, id.substring(0, index));
        siteVector.setValue(j, id.substring(index + 1));
      }
    }
    d.resolve(dataset);
  }).fail(function (err) {
    d.reject(err);
  });
  return d;
};
phantasus.SampleDatasets.prototype = {

  openTcga: function (options) {
    console.log("openTcga", options);
    this.callback({
      name: options.name,
      renderReady: function (heatMap) {
        var whitelist = [
          'age_at_initial_pathologic_diagnosis',
          'breast_carcinoma_estrogen_receptor_status',
          'breast_carcinoma_progesterone_receptor_status',
          'lab_proc_her2_neu_immunohistochemistry_receptor_status',
          'days_to_death', 'ethnicity', 'gender',
          'histological_type', 'pathologic_stage'];

        var columnMetadata = heatMap.getProject().getFullDataset().getColumnMetadata();
        for (var i = 0; i < whitelist.length; i++) {
          if (columnMetadata.getByName(whitelist[i])) {
            heatMap.addTrack(whitelist[i], true, 'color');
          }
        }
        // view in space of mutation sample ids only
        if (options.sig_genes) {
          if (heatMap.getTrackIndex('q_value', false) === -1) {
            heatMap.addTrack('q_value', false, 'text');
          }
        }
      },
      columns: [
        {
          field: 'participant_id',
          display: 'text'
        }, {
          field: 'sample_type',
          display: 'color'
        }, {
          field: 'mutation_summary',
          display: 'stacked_bar'
        }, {
          field: 'mutation_summary_selection',
          display: 'stacked_bar'
        }, {
          field: 'mRNAseq_cluster',
          display: 'color, highlight'
        }],
      dataset: phantasus.SampleDatasets.getTcgaDataset(options)
    });
  },
  openCCLE: function (options) {
    var name = [];
    if (options.sig_genes) {
      name.push('Mut');
    }
    if (options.cn) {
      name.push('CN');
    }
    if (options.mrna) {
      name.push('Exp');
    }
    if (options.ach) {
      name.push('Ach');
    }
    this.callback({
      rows: [
        {
          field: 'id',
          display: 'text'
        }, {
          field: 'Description',
          display: 'text, tooltip'
        }, {
          field: 'mutation_summary',
          display: 'stacked_bar'
        }, {
          field: 'Source',
          display: 'color'
        }],
      columns: [
        {
          field: 'id',
          display: 'text,tooltip'
        }, {
          field: 'mutation_summary',
          display: 'stacked_bar'
        }, {
          field: 'site',
          display: 'color, highlight'
        }],
      dataset: phantasus.SampleDatasets.getCCLEDataset(options),
      name: 'CCLE - ' + name.join(', ')
    });
  }
};

phantasus.SampleDatasets.TCGA_DISEASE_TYPES_INFO = [
  {
    id: 'mrna',
    name: 'GENE EXPRESSION',
    type: 'mrna'
  }, {
    id: 'gistic',
    name: 'GISTIC COPY NUMBER',
    type: 'gistic'
  }, {
    id: 'gistic',
    name: 'COPY NUMBER BY GENE',
    type: 'gisticGene'
  }, {
    id: 'sig_genes',
    name: 'MUTATION',
    type: 'sig_genes'
  }, {
    id: 'rppa',
    name: 'PROTEOMICS',
    type: 'rppa'
  }, {
    id: 'methylation',
    name: 'METHYLATION',
    type: 'methylation'
  }];

phantasus.AdjustDataTool = function () {
};
phantasus.AdjustDataTool.prototype = {
  toString: function () {
    return 'Adjust';
  },
  init: function (project, form) {
    var _this = this;
    form.$form.find('[name=scale_column_sum]').on('change', function (e) {
      form.setVisible('column_sum', form.getValue('scale_column_sum'));
    });
    form.setVisible('column_sum', false);

  },
  gui: function () {
    // z-score, robust z-score, log2, inverse log2
    return [{
      name: 'scale_column_sum',
      type: 'checkbox',
      help: 'Whether to scale each column sum to a specified value'
    }, {
      name: 'column_sum',
      type: 'text',
      style: 'max-width:150px;'
    }, {
      name: 'log_2',
      type: 'checkbox'
    }, {
      name: 'inverse_log_2',
      type: 'checkbox'
    }, {
      name: 'quantile_normalize',
      type: 'checkbox'
    }, {
      name: 'z-score',
      type: 'checkbox',
      help: 'Subtract mean, divide by standard deviation'
    }, {
      name: 'robust_z-score',
      type: 'checkbox',
      help: 'Subtract median, divide by median absolute deviation'
    }, {
      name: 'use_selected_rows_and_columns_only',
      type: 'checkbox'
    }];
  },
  execute: function (options) {
    var project = options.project;
    var heatMap = options.heatMap;

    if (options.input.log_2 || options.input.inverse_log_2
      || options.input['z-score'] || options.input['robust_z-score'] || options.input.quantile_normalize || options.input.scale_column_sum) {
      // clone the values 1st
      var sortedFilteredDataset = phantasus.DatasetUtil.copy(project
        .getSortedFilteredDataset());
      var rowIndices = project.getRowSelectionModel()
        .getViewIndices().values().sort(
          function (a, b) {
            return (a === b ? 0 : (a < b ? -1 : 1));
          });
      if (rowIndices.length === 0) {
        rowIndices = null;
      }
      var columnIndices = project.getColumnSelectionModel()
        .getViewIndices().values().sort(
          function (a, b) {
            return (a === b ? 0 : (a < b ? -1 : 1));
          });
      if (columnIndices.length === 0) {
        columnIndices = null;
      }
      var dataset = options.input.use_selected_rows_and_columns_only ? new phantasus.Slice
        : sortedFilteredDataset;
      var rowView = new phantasus.DatasetRowView(dataset);
      var functions = [];
      if (options.input.scale_column_sum) {
        var scaleToValue = parseFloat(options.input.column_sum);
        if (!isNaN(scaleToValue)) {
          for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
            var sum = 0;
            for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
              var value = dataset.getValue(i, j);
              if (!isNaN(value)) {
                sum += value;
              }
            }
            var ratio = scaleToValue / sum;
            for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
              var value = dataset.getValue(i, j);
              dataset.setValue(i, j, value * ratio);
            }
          }
        }
      }
      if (options.input.log_2) {
        for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
          for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
            dataset.setValue(i, j, phantasus.Log2(dataset.getValue(
              i, j)));
          }
        }
      }
      if (options.input.inverse_log_2) {
        for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
          for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
            var value = dataset.getValue(i, j);
            if (value >= 0) {
              dataset.setValue(i, j, Math.pow(2, value));
            }
          }
        }
      }
      if (options.input.quantile_normalize) {
        phantasus.QNorm.execute(dataset);
      }
      if (options.input['z-score']) {
        for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
          rowView.setIndex(i);
          var mean = phantasus.Mean(rowView);
          var stdev = Math.sqrt(phantasus.Variance(rowView));
          for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
            dataset.setValue(i, j, (dataset.getValue(i, j) - mean)
              / stdev);
          }
        }
      }
      if (options.input['robust_z-score']) {
        for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
          rowView.setIndex(i);
          var median = phantasus.Median(rowView);
          var mad = phantasus.MAD(rowView, median);
          for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
            dataset.setValue(i, j,
              (dataset.getValue(i, j) - median) / mad);
          }
        }
      }

      return new phantasus.HeatMap({
        name: heatMap.getName(),
        dataset: dataset,
        parent: heatMap,
        symmetric: project.isSymmetric() && dataset.getColumnCount() === dataset.getRowCount()
      });
    }
  }
};

phantasus.AnnotateDendrogramTool = function (isColumns) {
  this._isColumns = isColumns;
};
phantasus.AnnotateDendrogramTool.prototype = {
  toString: function () {
    return 'Annotate Dendrogram';
  },
  gui: function () {
    return [{
      name: 'file',
      value: '',
      type: 'file',
      required: true,
      help: 'an xlsx file or a tab-delimitted text file'
    }];
  },
  execute: function (options) {
    var fileOrUrl = options.input.file;
    var isColumns = this._isColumns;
    var heatMap = options.heatMap;
    var result = phantasus.Util.readLines(fileOrUrl);
    var fileName = phantasus.Util.getFileName(fileOrUrl);
    var dendrogram = isColumns ? heatMap.columnDendrogram
      : heatMap.rowDendrogram;
    var nameToNode = new phantasus.Map();
    phantasus.DendrogramUtil.dfs(dendrogram.tree.rootNode,
      function (node) {
        nameToNode.set(node.name, node);
        return true;
      });
    var tab = /\t/;
    result.done(function (lines) {
      var header = lines[0].split(tab);
      var promptTool = {};
      // node.info = {foo:['a', 'b'], bar:[3]}
      promptTool.execute = function (options) {
        var nodeIdField = options.input.node_id_field;
        var nodeIdIndex = _.indexOf(header, nodeIdField);
        var numberOfMatchingNodes = 0;
        for (var i = 1; i < lines.length; i++) {
          var array = lines[i].split(tab);
          var nodeName = array[nodeIdIndex];
          var node = nameToNode.get(nodeName);
          if (node !== undefined) {
            numberOfMatchingNodes++;
            var info = node.info || (node.info = {});
            for (var j = 0; j < array.length; j++) {
              if (j === nodeIdIndex) {
                continue;
              }
              var vals = info[header[j]];
              if (vals === undefined) {
                vals = [];
                info[header[j]] = vals;
              }
              vals.push(array[j]);
            }
          }
        }
        heatMap.trigger('dendrogramAnnotated', {
          isColumns: isColumns
        });
        dendrogram.setInvalid(true);
        dendrogram.repaint();
      };
      promptTool.toString = function () {
        return 'Select Node Id Field';
      };
      promptTool.gui = function () {
        return [{
          name: 'node_id_field',
          type: 'select',
          options: _.map(header, function (item) {
            return {
              name: item,
              value: item
            };
          }),
          required: true
        }];
      };
      phantasus.HeatMap.showTool(promptTool, heatMap);
    });
  }
};

/**
 * @param chartOptions.heatmap phantasus.HeatMap
 * @param chartOptions.project
 *            phantasus.Project
 */
phantasus.ChartTool = function (chartOptions) {
  var _this = this;
  this.project = chartOptions.project;
  this.heatmap = chartOptions.heatmap;
  var project = this.project;
  this.$el = $('<div class="container-fluid">'
    + '<div class="row">'
    + '<div data-name="configPane" class="col-xs-2"></div>'
    + '<div class="col-xs-10"><div style="position:relative;" data-name="chartDiv"></div></div>'
    + '</div></div>');

  var formBuilder = new phantasus.FormBuilder({
    formStyle: 'vertical'
  });
  this.formBuilder = formBuilder;
  formBuilder.append({
    name: 'chart_type',
    type: 'bootstrap-select',
    options: [
      'boxplot', 'row profile', 'column profile', 'row scatter matrix', 'column scatter' +
      ' matrix']
  });
  var rowOptions = [];
  var columnOptions = [];
  var numericRowOptions = [];
  var numericColumnOptions = [];
  var options = [];
  var numericOptions = [];
  var updateOptions = function () {
    var dataset = project.getFullDataset();
    rowOptions = [
      {
        name: '(None)',
        value: ''
      }];
    columnOptions = [
      {
        name: '(None)',
        value: ''
      }];
    numericRowOptions = [
      {
        name: '(None)',
        value: ''
      }];
    numericColumnOptions = [
      {
        name: '(None)',
        value: ''
      }];
    options = [
      {
        name: '(None)',
        value: ''
      }];
    numericOptions = [
      {
        name: '(None)',
        value: ''
      }];

    phantasus.MetadataUtil.getMetadataNames(dataset.getRowMetadata())
      .forEach(
        function (name) {
          var dataType = phantasus.VectorUtil
            .getDataType(dataset.getRowMetadata()
              .getByName(name));
          if (dataType === 'number'
            || dataType === '[number]') {
            numericRowOptions.push({
              name: name + ' (row)',
              value: name + '_r'
            });
          }
          rowOptions.push({
            name: name + ' (row)',
            value: name + '_r'
          });
        });

    phantasus.MetadataUtil.getMetadataNames(dataset.getColumnMetadata())
      .forEach(
        function (name) {
          var dataType = phantasus.VectorUtil
            .getDataType(dataset.getColumnMetadata()
              .getByName(name));
          if (dataType === 'number'
            || dataType === '[number]') {
            numericColumnOptions.push({
              name: name + ' (column)',
              value: name + '_c'
            });
          }
          columnOptions.push({
            name: name + ' (column)',
            value: name + '_c'
          });
        });

    options = options.concat(rowOptions.slice(1));
    options = options.concat(columnOptions.slice(1));

    numericOptions = numericOptions.concat(numericRowOptions.slice(1));
    numericOptions = numericOptions.concat(numericColumnOptions.slice(1));
  };

  updateOptions();
  formBuilder.append({
    name: 'group_by',
    type: 'bootstrap-select',
    options: options
  });

  formBuilder.append({
    name: 'axis_label',
    type: 'bootstrap-select',
    options: rowOptions
  });
  formBuilder.append({
    name: 'show_outliers',
    type: 'checkbox',
    value: true
  });

  formBuilder.append({
    name: 'color',
    type: 'bootstrap-select',
    options: options
  });

  // formBuilder.append({
  //   name: 'size',
  //   type: 'bootstrap-select',
  //   options: numericOptions
  // });
  formBuilder.append({
    name: 'tooltip',
    type: 'bootstrap-select',
    multiple: true,
    search: true,
    options: options.slice(1)
  });

  formBuilder.append({
    name: 'chart_width',
    type: 'range',
    value: 400,
    min: 60,
    max: 800,
    step: 10
  });
  formBuilder.append({
    name: 'chart_height',
    type: 'range',
    value: 400,
    min: 20,
    max: 800,
    step: 10
  });

  var chartTypeToParameter = {
    'row profile': {
      axis_label: 'columns',
      tooltip: 'columns',
      color: 'rows'
    },
    'column profile': {
      axis_label: 'rows',
      tooltip: 'rows',
      color: 'columns'
    },
    'row scatter matrix': {
      axis_label: 'rows',
      color: 'columns',
      tooltip: 'columns'
    },
    'column scatter matrix': {
      axis_label: 'columns',
      color: 'rows',
      tooltip: 'rows'
    },
    boxplot: {
      group_by: true,
      show_outliers: true,
      tooltip: 'both'
    }
  };

  function setVisibility() {
    var chartType = formBuilder.getValue('chart_type');
    var chartOptions = chartTypeToParameter[chartType];
    if (chartOptions.axis_label != null) {
      formBuilder.setOptions('axis_label',
        chartOptions.axis_label === 'rows' ? rowOptions : columnOptions,
        true);

    }
    formBuilder.setVisible('color', chartOptions.color != null);
    formBuilder.setVisible('axis_label', chartOptions.axis_label != null);
    formBuilder.setVisible('group_by', chartOptions.group_by);
    formBuilder.setVisible('show_outliers', chartOptions.show_outliers);
    formBuilder.setOptions('tooltip', chartOptions.tooltip === 'rows' ? rowOptions.slice(1) : (chartOptions.tooltip === 'columns' ? columnOptions.slice(1) : options));
    formBuilder.setOptions('color', chartOptions.color === 'rows' ? rowOptions : (chartOptions.color === 'columns' ? columnOptions : options));
  }

  this.tooltip = [];
  var draw = function () {
    _.debounce(_this.draw(), 100);
  };
  formBuilder.$form.on('change', 'select,input[type=range]', function (e) {
    if ($(this).attr('name') === 'tooltip') {
      var tooltipVal = _this.formBuilder.getValue('tooltip');
      _this.tooltip.length = 0; // clear array
      if (tooltipVal != null) {
        tooltipVal.forEach(function (tip) {
          _this.tooltip.push(phantasus.ChartTool.getVectorInfo(tip));
        });
      }
    } else {
      setVisibility();
      draw();
    }

  });

  formBuilder.$form.on('click', 'input[type=checkbox]', function (e) {
    draw();

  });
  formBuilder.$form.on('keypress', 'input[type=text]', function (e) {
    if (e.which === 13) {
      draw();
    }
  });

  setVisibility();

  var trackChanged = function () {
    updateOptions();
    setVisibility();
    formBuilder.setOptions('group_by', options, true);
  };

  project.getColumnSelectionModel().on('selectionChanged.chart', draw);
  project.getRowSelectionModel().on('selectionChanged.chart', draw);
  project.on('trackChanged.chart', trackChanged);
  this.$chart = this.$el.find('[data-name=chartDiv]');
  var $dialog = $('<div style="background:white;" title="Chart"></div>');
  var $configPane = this.$el.find('[data-name=configPane]');
  formBuilder.$form.appendTo($configPane);
  this.$el.appendTo($dialog);
  $dialog.dialog({
    dialogClass: 'phantasus',
    close: function (event, ui) {
      project.off('trackChanged.chart', trackChanged);
      project.getRowSelectionModel().off('selectionChanged.chart', draw);
      project.getColumnSelectionModel().off('selectionChanged.chart',
        draw);
      $dialog.dialog('destroy').remove();
      event.stopPropagation();
    },

    resizable: true,
    height: 600,
    width: 900
  });
  this.$dialog = $dialog;
  this.draw();
};

phantasus.ChartTool.getVectorInfo = function (value) {
  var field = value.substring(0, value.length - 2);
  var isColumns = value.substring(value.length - 2) === '_c';
  return {
    field: field,
    isColumns: isColumns
  };
};
phantasus.ChartTool.prototype = {
  /**
   *
   * @param options.dataset
   * @param options.colorByVector
   * @param options.colorModel
   * @param options.transpose
   * @param options.chartWidth
   * @param options.chartHeight
   * @param options.axisLabelVector
   * @private
   */
  _createScatter: function (options) {
    var _this = this;
    var dataset = options.dataset;
    var colorByVector = options.colorByVector;
    var colorModel = options.colorModel;
    var heatmap = this.heatmap;
    var chartWidth = options.chartWidth;
    var chartHeight = options.chartHeight;
    var axisLabelVector = options.axisLabelVector; // for row scatter, row vector
    var transpose = options.transpose;
    var chart = {
      animation: false,
      toolbox: {
        feature: {
          brush: {
            title: {
              rect: 'Rectangle selection',
              polygon: 'Polygon selection',
              clear: 'Clear Selection',
              keep: 'Keep previous selection'
            }
          }
        }
      },
      brush: {
        brushLink: 'all',
        xAxisIndex: [],
        yAxisIndex: [],
        inBrush: {
          opacity: 1
        }
      },
      tooltip: {
        trigger: 'item'
      },
      grid: [],
      xAxis: [],
      yAxis: [],
      series: []
    };
    var index = 0;
    var GAP = 20;
    var BASE_LEFT = 40;
    var BASE_TOP = 40;
    // rowIndexOne on x, rowIndexTwo on y
    for (var rowIndexOne = 1, nrows = dataset.getRowCount(); rowIndexOne < nrows; rowIndexOne++) {
      for (var rowIndexTwo = 0; rowIndexTwo < rowIndexOne; rowIndexTwo++) {
        (function () {
          var data = [];
          var color = [];
          for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
            data.push([dataset.getValue(rowIndexOne, j), dataset.getValue(rowIndexTwo, j), j]);
          }

          if (colorByVector) {
            for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
              var colorByValue = colorByVector.getValue(j);
              color.push(colorModel.getMappedValue(colorByVector,
                colorByValue));
            }
          }
          chart.grid.push({
            left: GAP + (rowIndexOne - 1) * (chartWidth + GAP),
            top: BASE_TOP + rowIndexTwo * (chartHeight + GAP),
            width: chartWidth,
            height: chartHeight
          });

          chart.brush.xAxisIndex && chart.brush.xAxisIndex.push(index);
          chart.brush.yAxisIndex && chart.brush.yAxisIndex.push(index);
          chart.xAxis.push({
            splitNumber: 3,
            position: 'top',
            name: axisLabelVector != null && rowIndexTwo === 0 ? axisLabelVector.getValue(rowIndexOne) : '',
            nameGap: 25,
            nameLocation: 'middle',
            axisLine: {
              show: false,
              onZero: false
            },
            axisTick: {
              show: rowIndexTwo === 0,
              inside: true
            },
            axisLabel: {
              show: rowIndexTwo === 0
            },
            type: 'value',
            gridIndex: index,
            scale: true
          });

          chart.yAxis.push({
            splitNumber: 3,
            nameGap: 50,
            name: axisLabelVector != null && rowIndexOne === nrows - 1 ? axisLabelVector.getValue(rowIndexTwo) : '',
            nameLocation: 'middle',
            position: 'right',
            axisLine: {
              show: false,
              onZero: false
            },
            axisTick: {
              show: rowIndexOne === nrows - 1,
              inside: true
            },
            axisLabel: {
              show: rowIndexOne === nrows - 1
            },
            type: 'value',
            gridIndex: index,
            scale: true
          });

          chart.series.push({
            symbolSize: 4,
            tooltip: {
              formatter: function (obj) {
                var value = obj.value;
                var s = [];
                s.push('x: ' + _this.heatmap.getHeatMapElementComponent().getDrawValuesFormat()(value[0]));
                s.push('<br>');
                s.push('y ' + _this.heatmap.getHeatMapElementComponent().getDrawValuesFormat()(value[1]));
                if (transpose) {
                  phantasus.ChartTool.getTooltip({
                    text: s,
                    tooltip: _this.tooltip,
                    dataset: new phantasus.TransposedDatasetView(dataset),
                    rowIndex: value[2],
                    columnIndex: -1
                  });
                } else {
                  phantasus.ChartTool.getTooltip({
                    text: s,
                    tooltip: _this.tooltip,
                    dataset: dataset,
                    rowIndex: -1,
                    columnIndex: value[2]
                  });
                }
                return s.join('');
              }
            },
            itemStyle: {
              normal: {
                color: function (param) {
                  return color.length === 0 ? '#1f78b4' : color[param.dataIndex];
                }
              }
            },
            type: 'scatter',
            xAxisIndex: index,
            yAxisIndex: index,
            data: data
          });
          index++;
        })();

      }
    }
    var myChart = echarts.init(options.el);
    myChart.setOption(chart);

  },
  /**
   *
   * @param options.dataset
   * @param options.colorByVector
   * @param options.colorModel
   * @param options.transpose
   * @param options.chartWidth
   * @param options.chartHeight
   * @param options.axisLabelVector
   * @private
   */
  _createProfile: function (options) {
    var _this = this;
    var dataset = options.dataset;
    var colorByVector = options.colorByVector;
    var colorModel = options.colorModel;
    var heatmap = this.heatmap;
    var chartWidth = options.chartWidth;
    var chartHeight = options.chartHeight;
    var axisLabelVector = options.axisLabelVector; // for row scatter, row vector
    var transpose = options.transpose;
    var axisLabel = [];
    var rotateXAxis = 0;
    var bottomPadding = 60;
    for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
      axisLabel.push(axisLabelVector != null ? axisLabelVector.getValue(j) : '' + j);
    }

    if (dataset.getColumnCount() >= 5 &&
        axisLabel.every(function (val) { return val.length >= 2;}) ||
        axisLabel.some(function (val) { return val.length >= 7;})) {
      rotateXAxis = 45;
      bottomPadding = 150;
    }

    var series = [];
    var colorMap = phantasus.VectorColorModel.getColorMapForNumber(dataset.getRowCount());
    for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
      // each row is a new trace
      var colorByValue = colorByVector != null ? colorByVector.getValue(i) : '' + i;
      var color = colorByVector != null ? colorModel.getMappedValue(colorByVector, colorByValue) : colorMap[i % colorMap.length];
      var data = [];
      for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
        data.push([j, dataset.getValue(i, j), i]);
      }
      series.push({
        name: colorByValue,
        type: 'line',
        data: data,
        tooltip: {
          formatter: function (obj) {
            var value = obj.value;
            var s = [];
            s.push(_this.heatmap.getHeatMapElementComponent().getDrawValuesFormat()(value[1]));
            phantasus.ChartTool.getTooltip({
              text: s,
              tooltip: _this.tooltip,
              dataset: dataset,
              rowIndex: value[2],
              columnIndex: value[0]
            });
            return s.join('');
          }
        }
      });
    }

    var chart = {
      legend: {
        orient: 'vertical',
        left: 'right',
        top: 2,
        itemWidth: 14,
        height: dataset.getRowCount() * 25,
        data: series.map(function (s) {
          return s.name;
        })
      },
      animation: false,
      tooltip: {
        trigger: 'item'
      },
      xAxis: {
        type: 'category',
        data: axisLabel,
        axisLabel: {
          rotate: rotateXAxis,
          interval: 0
        },
        axisTick: {
          alignWithLabel: true
        }
      },
      yAxis: {
        axisLine: {
          show: true,
          onZero: false
        },
        type: 'value',
        name: ''
      },
      grid: {right: 120, bottom: bottomPadding},
      series: series
    };

    var myChart = echarts.init(options.el);
    myChart.setOption(chart);
  },
  /**
   *
   * @param options.datasets 1-d array of datasets
   * @param options.ids 1-d array of grouping values
   * @param options.showOutliers
   * @param options.el
   * @private
   */
  _createBoxPlot: function (options) {
    var _this = this;
    var showPoints = options.showPoints;
    var datasets = options.datasets;
    var ids = options.ids;
    var heatmap = this.heatmap;
    var size = 6;
    var boxData = [];
    var outliers = [];

    for (var k = 0, ndatasets = datasets.length; k < ndatasets; k++) {
      var dataset = datasets[k];
      var id = ids[k];
      var values = new Float32Array(dataset.getRowCount() * dataset.getColumnCount());
      var counter = 0;
      for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
        for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
          var value = dataset.getValue(i, j);
          if (!isNaN(value)) {
            values[counter] = value;
            counter++;
          }
        }
      }
      if (counter !== values.length) {
        values = values.slice(0, counter);
      }
      values.sort();
      // [min,  Q1,  median,  Q3,  max]
      var item = phantasus.BoxPlotArrayItem(values);
      boxData.push([item.lowerAdjacentValue, item.q1, item.median, item.q3, item.upperAdjacentValue]);
      if (options.showOutliers) {
        var w = 1.5;
        var upperOutlier = item.q3 + w * (item.q3 - item.q1);
        var lowerOutlier = item.q1 - w * (item.q3 - item.q1);
        for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
          for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
            var value = dataset.getValue(i, j);
            if (value > upperOutlier || value < lowerOutlier) {
              outliers.push([id, value, i, j]);
            }
          }
        }
      }
    }
    var chart = {
      animation: false,
      tooltip: {
        trigger: 'item'
      },
      xAxis: {
        type: 'category',
        data: ids
      },
      yAxis: {
        axisLine: {
          show: true,
          onZero: false
        },
        type: 'value',
        name: ''
      },
      series: [
        {
          animationDuration: 0,
          hoverAnimation: false,
          name: 'boxplot',
          type: 'boxplot',
          data: boxData,
          tooltip: {
            formatter: function (param) {
              var text = [];
              if (param.name !== '') {
                text.push(param.name);
              }

              text = text.concat([
                'upper: ' + _this.heatmap.getHeatMapElementComponent().getDrawValuesFormat()(param.data[4]),
                'Q3: ' + _this.heatmap.getHeatMapElementComponent().getDrawValuesFormat()(param.data[3]),
                'median: ' + _this.heatmap.getHeatMapElementComponent().getDrawValuesFormat()(param.data[2]),
                'Q1: ' + _this.heatmap.getHeatMapElementComponent().getDrawValuesFormat()(param.data[1]),
                'lower: ' + _this.heatmap.getHeatMapElementComponent().getDrawValuesFormat()(param.data[0])
              ]);
              return text.join('<br/>');
            }
          }
        },
        {
          name: 'outlier',
          type: 'scatter',
          data: outliers,
          symbolSize: 5,
          itemStyle: {
            normal: {
              borderWidth: 1,
              borderColor: 'black',
              opacity: 0.8
            }
          },
          tooltip: {
            formatter: function (obj) {
              var value = obj.value;
              var s = [];
              if (value[0] !== '') {
                s.push(value[0]); // name
                s.push('<br>');
              }
              s.push(_this.heatmap.getHeatMapElementComponent().getDrawValuesFormat()(value[1]));
              phantasus.ChartTool.getTooltip({
                text: s,
                tooltip: _this.tooltip,
                dataset: dataset,
                rowIndex: value[2],
                columnIndex: value[3]
              });
              return s.join('');
            }
          }
        }
      ]
    };

    var myChart = echarts.init(options.el);
    myChart.setOption(chart);
  },
  draw: function () {
    var _this = this;
    this.$chart.empty();
    // 140 to 800
    var gridWidth = parseInt(this.formBuilder.getValue('chart_width'));
    var gridHeight = parseInt(this.formBuilder.getValue('chart_height'));
    var showOutliers = this.formBuilder.getValue('show_outliers');

    var groupBy = this.formBuilder.getValue('group_by');
    var axisLabel = this.formBuilder.getValue('axis_label');

    // var sizeBy = this.formBuilder.getValue('size');
    var chartType = this.formBuilder.getValue('chart_type');

    var dataset = this.project.getSelectedDataset({
      emptyToAll: false
    });

    this.dataset = dataset;
    if (dataset.getRowCount() === 0 && dataset.getColumnCount() === 0) {
      $('<h4>Please select rows and columns in the heat map.</h4>')
        .appendTo(this.$chart);
      return;
    } else if (dataset.getRowCount() === 0) {
      $('<h4>Please select rows in the heat map.</h4>')
        .appendTo(this.$chart);
      return;
    }
    if (dataset.getColumnCount() === 0) {
      $('<h4>Please select columns in the heat map.</h4>')
        .appendTo(this.$chart);
      return;
    }

    var heatmap = this.heatmap;
    // var sizeByInfo = phantasus.ChartTool.getVectorInfo(sizeBy);

    // var sizeByVector = sizeByInfo.isColumns ? dataset.getColumnMetadata().getByName(sizeByInfo.field) : dataset.getRowMetadata().getByName(
    //   sizeByInfo.field);

    var axisLabelInfo = phantasus.ChartTool.getVectorInfo(axisLabel);
    var axisLabelVector = axisLabelInfo.isColumns ? dataset.getColumnMetadata().getByName(axisLabelInfo.field) : dataset.getRowMetadata().getByName(
      axisLabelInfo.field);

    var rowIds = [undefined];
    var columnIds = [undefined];
    var colorBy = this.formBuilder.getValue('color');
    var colorByVector = null;
    var colorModel = null;
    if (colorBy != null) {
      var colorByInfo = phantasus.ChartTool.getVectorInfo(colorBy);
      colorModel = !colorByInfo.isColumns ? this.project.getRowColorModel()
        : this.project.getColumnColorModel();
      colorByVector = colorByInfo.isColumns ? dataset.getColumnMetadata().getByName(colorByInfo.field) : dataset.getRowMetadata().getByName(
        colorByInfo.field);
    }

    if (chartType === 'row profile' || chartType === 'column profile') {
      var transpose = chartType === 'column profile';
      if (transpose) {
        dataset = new phantasus.TransposedDatasetView(dataset);
      }
      if (dataset.getRowCount() > 100) {
        $('<h4>Maximum chart size exceeded.</h4>')
          .appendTo(this.$chart);
        return;
      }
      // add horizontal space for legend
      var $chart = $('<div style="width: ' + (gridWidth + 120) + 'px;height:' + gridHeight + 'px;"></div>');
      $chart.appendTo(this.$chart);
      this._createProfile({
        width: gridWidth,
        el: $chart[0],
        dataset: dataset,
        chartWidth: gridWidth,
        chartHeight: gridHeight,
        transpose: transpose,
        colorModel: colorModel,
        colorByVector: colorByVector,
        axisLabelVector: axisLabelVector
      });
    } else if (chartType === 'row scatter matrix' || chartType === 'column scatter matrix') {
      var transpose = chartType === 'column scatter matrix';

      if (transpose) {
        dataset = new phantasus.TransposedDatasetView(dataset);
      }
      if (dataset.getRowCount() > 20) {
        $('<h4>Maximum chart size exceeded.</h4>')
          .appendTo(this.$chart);
        return;
      }
      var $chart = $('<div style="width:' + (80 + (dataset.getRowCount() - 1) * (gridWidth + 20)) + 'px;height:' + (80 + dataset.getRowCount() * (gridHeight + 20)) +
        'px;"></div>');
      $chart.appendTo(this.$chart);
      this._createScatter({
        el: $chart[0],
        dataset: dataset,
        chartWidth: gridWidth,
        chartHeight: gridHeight,
        transpose: transpose,
        colorModel: colorModel,
        colorByVector: colorByVector,
        axisLabelVector: axisLabelVector
      });
    } else if (chartType === 'boxplot') {
      var datasets = [];//1-d array of datasets
      var ids = []; // 1-d array of grouping values
      if (groupBy) {
        var groupByInfo = phantasus.ChartTool
          .getVectorInfo(groupBy);
        var vector = groupByInfo.isColumns ? dataset
            .getColumnMetadata().getByName(groupByInfo.field)
          : dataset.getRowMetadata().getByName(
            groupByInfo.field);
        var isArray = phantasus.VectorUtil.getDataType(vector)[0] === '[';
        var valueToIndices = phantasus.VectorUtil.createValueToIndicesMap(vector, true);
        valueToIndices.forEach(function (indices, value) {
          datasets.push(new phantasus.SlicedDatasetView(dataset, groupByInfo.isColumns ? null : indices, groupByInfo.isColumns ? indices : null));
          ids.push(value);
        });
      } else {
        datasets.push(dataset);
        ids.push('');
      }
      // sort rows and columns by median
      // if (gridRowCount > 1) {
      //   var summary = [];
      //   for (var i = 0; i < gridRowCount; i++) {
      //     summary[i] = [];
      //     var gridRow = grid[i];
      //     for (var j = 0; j < gridColumnCount; j++) {
      //       var array = gridRow[j];
      //       var values = [];
      //       if (array) {
      //         for (var k = 0, nitems = array.length; k < nitems; k++) {
      //           var item = array[k];
      //           var value = dataset.getValue(item.row, item.column);
      //           if (!isNaN(value)) {
      //             values.push(value);
      //           }
      //
      //         }
      //       }
      //       summary[i][j] = phantasus.Median(phantasus.VectorUtil.arrayAsVector(values));
      //     }
      //   }
      //   // sort rows
      //   var rowMedians = [];
      //   for (var i = 0; i < gridRowCount; i++) {
      //     var values = [];
      //     for (var j = 0; j < gridColumnCount; j++) {
      //       values.push(summary[i][j]);
      //     }
      //     rowMedians.push(phantasus.Median(phantasus.VectorUtil.arrayAsVector(values)));
      //   }
      //
      //   var newRowOrder = phantasus.Util.indexSort(rowMedians, false);
      //   var newRowIds = [];
      //   var newGrid = [];
      //   for (var i = 0; i < gridRowCount; i++) {
      //     newGrid.push(grid[newRowOrder[i]]);
      //     newRowIds.push(rowIds[newRowOrder[i]]);
      //   }
      //   grid = newGrid;
      //   rowIds = newRowIds;
      // }

      var $chartEl = $('<div style="width: ' + gridWidth + 'px;height:' + gridHeight + 'px;"></div>');
      $chartEl.appendTo(this.$chart);
      this._createBoxPlot({
        showOutliers: showOutliers,
        el: $chartEl[0],
        datasets: datasets,
        ids: ids
      });
    }
  }
};

/**
 *
 * @param options.dataset
 * @param options.text
 * @param options.rowIndex
 * @param options.columnIndex
 */
phantasus.ChartTool.getTooltip = function (options) {
  for (var tipIndex = 0; tipIndex < options.tooltip.length; tipIndex++) {
    var tip = options.tooltip[tipIndex];
    var metadata;
    var index;
    if (tip.isColumns) {
      metadata = options.dataset.getColumnMetadata();
      index = options.columnIndex;
    } else {
      metadata = options.dataset.getRowMetadata();
      index = options.rowIndex;
    }
    if (index !== -1) {
      var v = metadata.getByName(tip.field);
      phantasus.HeatMapTooltipProvider.vectorToString(v,
        index, options.text, '<br>');
    }
  }
};


phantasus.CollapseDatasetTool = function () {
};
phantasus.CollapseDatasetTool.Functions = [phantasus.Mean, phantasus.Median,
  new phantasus.MaxPercentiles([25, 75]), phantasus.Min, phantasus.Max, phantasus.Percentile, phantasus.Sum,
  phantasus.MaximumMeanProbe, phantasus.MaximumMedianProbe];
phantasus.CollapseDatasetTool.Functions.fromString = function (s) {
  for (var i = 0; i < phantasus.CollapseDatasetTool.Functions.length; i++) {
    if (phantasus.CollapseDatasetTool.Functions[i].toString() === s) {
      return phantasus.CollapseDatasetTool.Functions[i];
    }
  }
  throw new Error(s + ' not found');
};
phantasus.CollapseDatasetTool.prototype = {
  toString: function () {
    return 'Collapse';
  },
  init: function (project, form) {
    var setValue = function (val) {
      var isRows = val === 'Rows';
      var names = phantasus.MetadataUtil.getMetadataNames(isRows ? project
      .getFullDataset().getRowMetadata() : project
      .getFullDataset().getColumnMetadata());
      form.setOptions('collapse_to_fields', names);
    };
    form.$form.find('[name=collapse]').on('change', function (e) {
      setValue($(this).val());
    });
    form.setVisible('percentile', false);
    form.$form.find('[name=collapse_method]').on('change', function (e) {
      form.setVisible('percentile', $(this).val() === phantasus.Percentile.toString());
      form.setVisible('collapse', !phantasus.CollapseDatasetTool.Functions.fromString($(this).val()).selectOne)
    });


    setValue('Rows');
  },
  gui: function () {
    return [{
      name: 'collapse_method',
      options: phantasus.CollapseDatasetTool.Functions,
      value: phantasus.CollapseDatasetTool.Functions[1].toString(),
      type: 'select'
    }, {
      name: 'percentile',
      value: 75,
      type: 'text'
    }, {
      name: 'collapse',
      options: ['Columns', 'Rows'],
      value: 'Rows',
      type: 'radio'
    }, {
      name: 'collapse_to_fields',
      options: [],
      type: 'select',
      multiple: true
    }];
  },
  execute: function (options) {
    var project = options.project;
    var heatMap = options.heatMap;
    var f = phantasus.CollapseDatasetTool.Functions
    .fromString(options.input.collapse_method);
    if (f.toString() === phantasus.Percentile.toString()) {
      var p = parseFloat(options.input.percentile);
      f = function (vector) {
        return phantasus.Percentile(vector, p);
      };
    }

    var collapseToFields = options.input.collapse_to_fields;
    if (!collapseToFields || collapseToFields.length === 0) {
      throw new Error('Please select one or more fields to collapse to');
    }
    var dataset = project.getFullDataset();
    var rows = options.input.collapse == 'Rows';
    if (!rows) {
      dataset = new phantasus.TransposedDatasetView(dataset);
    }
    var allFields = phantasus.MetadataUtil.getMetadataNames(dataset
      .getRowMetadata());
    var collapseMethod = f.selectOne ? phantasus.SelectRow : phantasus.CollapseDataset;
    dataset = collapseMethod(dataset, collapseToFields, f, true);
    if (!rows) {
      dataset = phantasus.DatasetUtil.copy(new phantasus.TransposedDatasetView(dataset));
    }
    var set = new phantasus.Map();
    _.each(allFields, function (field) {
      set.set(field, true);
    });
    _.each(collapseToFields, function (field) {
      set.remove(field);
    });
    // hide fields that were not part of collapse to
    // console.log("Collapse ", set);
    // set.forEach(function (val, name) {
    //   heatMap.setTrackVisible(name, false, !rows);
    // });
    return new phantasus.HeatMap({
      name: heatMap.getName(),
      dataset: dataset,
      parent: heatMap,
      symmetric: false
    });
  }
};

phantasus.CreateAnnotation = function () {
};
phantasus.CreateAnnotation.prototype = {
  toString: function () {
    return 'Create Calculated Annotation';
  },
  gui: function () {
    return [
      {
        name: 'annotate',
        options: ['Columns', 'Rows'],
        value: 'Rows',
        type: 'radio'
      },
      {
        name: 'annotation_name',
        value: '',
        type: 'text',
        required: true,
        autocomplete: 'off'
      },
      {
        name: 'formula',
        value: '',
        type: 'textarea',
        placeholder: 'e.g MAD()',
        required: true,
        help: 'JavaScript formula. Built-in functions (case-sensitive): COUNTIF(expression),' +
        ' MAD(), MAX(),' +
        ' MEAN(), MEDIAN(), MIN(), PERCENTILE(p), SUM(), VARIANCE(). Refer to a field using FIELD(name)'
      }, {
        name: 'use_selected_rows_and_columns_only',
        type: 'checkbox'
      }];
  },
  execute: function (options) {
    var __project = options.project;
    var isColumns = options.input.annotate == 'Columns';
    var __formula = options.input.formula;
    var __dataset = options.input.use_selected_rows_and_columns_only ? __project
        .getSelectedDataset()
      : __project.getSortedFilteredDataset();
    if (isColumns) {
      __dataset = phantasus.DatasetUtil.transposedView(__dataset);
    }
    var __rowView = new phantasus.DatasetRowView(__dataset);
    var __vector = __dataset.getRowMetadata().add(
      options.input.annotation_name);

    var COUNTIF = function (val) {
      return phantasus.CountIf(__rowView, val);
    };
    var MAD = function () {
      return phantasus.MAD(__rowView);
    };
    var MAX = function () {
      return phantasus.Max(__rowView);
    };
    var MEAN = function () {
      return phantasus.Mean(__rowView);
    };
    var MEDIAN = function (p) {
      return phantasus.Percentile(__rowView, 50);
    };
    var MIN = function () {
      return phantasus.Min(__rowView);
    };
    var PERCENTILE = function (p) {
      return phantasus.Percentile(__rowView, p);
    };
    var SUM = function () {
      return phantasus.Sum(__rowView);
    };
    var VARIANCE = function () {
      return phantasus.Variance(__rowView);
    };
    var __index = 0;
    var FIELD = function (field) {
      var vector = __dataset.getRowMetadata().getByName(field);
      return vector ? vector.getValue(__index) : undefined;
    };

    for (var size = __dataset.getRowCount(); __index < size; __index++) {
      __rowView.setIndex(__index);
      var __val = eval(__formula);
      if (typeof __val === 'function') {
        __val = '';
      }
      __vector.setValue(__index, __val);
    }
    phantasus.VectorUtil.maybeConvertStringToNumber(__vector);
    __project.trigger('trackChanged', {
      vectors: [__vector],
      display: ['text'],
      columns: isColumns
    });
  }
};

phantasus.DendrogramEnrichmentTool = function (isColumns) {
  this.isColumns = isColumns;
};

phantasus.DendrogramEnrichmentTool.prototype = {
  toString: function () {
    return 'Dendrogram Enrichment';
  },
  gui: function (project) {
    var dataset = project.getSortedFilteredDataset();
    var fields = phantasus.MetadataUtil
      .getMetadataNames(this.isColumns ? dataset.getColumnMetadata()
        : dataset.getRowMetadata());
    return [{
      name: 'field',
      options: fields,
      type: 'bootstrap-select',
      multiple: false
    }, {
      name: 'min_p-value_for_enrichment',
      type: 'text',
      value: '0.05'
    }, {
      name: 'minimum_number_of_total_members_in_group',
      type: 'text',
      value: '5'
    }, {
      name: 'minimum_number_of_members_in_group',
      type: 'text',
      value: '3'
    }];
  },
  execute: function (options) {
    var project = options.project;
    var heatMap = options.heatMap;
    var pValue = options.input['min_p-value_for_enrichment'];
    var minTotalGroupSize = options.input.minimum_number_of_total_members_in_group;
    var minGroupSize = options.input.minimum_number_of_members_in_group;
    var dataset = project.getSortedFilteredDataset();
    var dendrogram = this.isColumns ? heatMap.columnDendrogram
      : heatMap.rowDendrogram;
    var vector = this.isColumns ? dataset.getColumnMetadata().getByName(
      options.input.field) : dataset.getRowMetadata().getByName(
      options.input.field);

    var valueToIndices = phantasus.VectorUtil
      .createValueToIndicesMap(vector);
    var valueToGlobalCount = new phantasus.Map();
    var values = [];
    valueToIndices.forEach(function (indices, value) {
      valueToGlobalCount.set(value, indices.length);
      values.push(value);
    });
    var nvalues = values.length;
    var N = vector.size();

    phantasus.DendrogramUtil.dfs(dendrogram.tree.rootNode,
      function (node) {
        delete node.info;
        var valueToCount = new phantasus.Map();
        for (var i = 0; i < nvalues; i++) {
          valueToCount.set(values[i], 0);
        }
        var min = node.minIndex;
        var max = node.maxIndex;
        var n = max - min + 1;
        if (n > 1 && n >= minTotalGroupSize) {
          for (var i = min; i <= max; i++) {
            var value = vector.getValue(i);
            valueToCount
              .set(value, valueToCount.get(value) + 1);
          }
          for (var i = 0; i < nvalues; i++) {
            var K = valueToGlobalCount.get(values[i]);
            var k = valueToCount.get(values[i]);
            if (k >= minGroupSize) {
              var a = k;
              var b = K - k;
              var c = n - k;
              var d = N + k - n - K;
              var p = phantasus.FisherExact.fisherTest(a, b,
                c, d);
              if (p <= pValue) {
                if (!node.info) {
                  node.info = {};
                }
                node.info[values[i]] = p;

              }
            }
          }
        }
        return true;
      });
    dendrogram.setInvalid(true);
    dendrogram.repaint();
  }
};

phantasus.DevAPI = function () {
};
phantasus.DevAPI.prototype = {
  toString: function () {
    return 'API';
  },
  gui: function () {
    return [{
      name: 'code',
      value: '',
      type: 'textarea',
      required: true,
      help: 'Enter your code'
    }];
  },
  execute: function (options) {
    var heatMap = options.heatMap;
    var code = options.input.code;
    eval(code);
    // force a repaint of everything
    heatMap.getProject().setFullDataset(heatMap.getProject().getFullDataset(), true);
  }
};

var ENRICHR_URL = 'https://amp.pharm.mssm.edu/Enrichr/addList';
var ENRICHR_SUBMIT_LIMIT = 10000;

phantasus.enrichrTool = function (project) {
  var self = this;

  var dataset = project.getSortedFilteredDataset();
  var rows = phantasus.MetadataUtil.getMetadataNames(dataset
    .getRowMetadata(), true);

  var $dialog = $('<div style="background:white;" title="Submit to Enrichr"></div>');
  var form = new phantasus.FormBuilder({
    formStyle: 'vertical'
  });
  form.appendContent('<h4>Please select rows.</h4>');

  [{
    name: 'column_with_gene_symbols',
    options: rows,
    value: _.first(rows),
    type: 'select'
  }, {
    name: 'ambiguous_genes_strategy',
    type: 'select',
    tooltipHelp: 'Sometimes gene symbol cell contains multiple values separated by \'///\'.',
    options: [{
      name: 'omit',
      value: 'omit'
    }, {
      name: 'split',
      value: 'split'
    }]
  }, {
    name: 'preview_data',
    type: 'select',
    style: 'height: auto; max-height: 600px; overflow-x: auto; overflow-y: hidden;',
    multiple: true,
    disabled: true, //READ-ONLY
  }].forEach(function (a) {
    form.append(a);
  });
  form.$form.appendTo($dialog);

  var columnSelect = form.$form.find("[name=column_with_gene_symbols]");
  var strategySelect = form.$form.find("[name=ambiguous_genes_strategy]");
  var previewData = form.$form.find('[name=preview_data]');

  var onSelect = function () {
    var data = phantasus.Dataset.toJSON(project.getSelectedDataset());
    var newOptions = [];

    if (data.rows < ENRICHR_SUBMIT_LIMIT) {
      var parsedData = parseData(data, $(columnSelect).val(), $(strategySelect).val());
      newOptions = _.size(parsedData) === 0 ? ['[No rows selected]'] : [parsedData.join(' ')];
    } else {
      newOptions = ['[Invalid amount of rows selected (0 < n < 10000)]'];
    }

    form.setOptions('preview_data', newOptions);
    //previewData[0].size = Math.max(newOptions.length, 5);
    previewData[0].size = 2;
  };

  columnSelect.on('change', onSelect);
  strategySelect.on('change', onSelect);

  onSelect();
  form.$form.find('[data-toggle="tooltip"]').tooltip();
  form.appendContent('Result will open in a new window automatically.');


  project.getRowSelectionModel().on("selectionChanged.chart", onSelect);
  $dialog.dialog({
    close: function (event, ui) {
      project.getRowSelectionModel().off("selectionChanged.chart", onSelect);
      $dialog.dialog('destroy').remove();
    },

    resizable: true,
    height: 450,
    width: 600,
    buttons: [
      {
        text: "Cancel",
        click: function () {
          $(this).dialog("close");
        }
      },
      {
        text: "Submit",
        click: function () {
          self.execute({
            project: project,
            form: form
          });
          $(this).dialog("close");
        }
      }
    ]
  });
  this.$dialog = $dialog;
};


function parseData(data, column_with_gene_symbols, strategy) {
  var targetData = (_.findWhere(data.rowMetadataModel.vectors, {name: column_with_gene_symbols}) || {array: []}).array;

  return _.reduce(targetData, function (acc, geneRow) {
    if (!_.isString(geneRow)) { // rows are not strings???
      acc.push(geneRow);
      return acc;
    }

    var isAmbiguousGene = geneRow.indexOf('///') !== -1;
    if (isAmbiguousGene && strategy === 'split') {
      acc = acc.concat(geneRow.split('///'));
    } else if (!isAmbiguousGene) {
      acc.push(geneRow);
    }

    return acc;
  }, []);
}


phantasus.enrichrTool.prototype = {
  toString: function () {
    return 'Submit to Enrichr';
  },
  execute: function (options) {
    var data = phantasus.Dataset.toJSON(options.project.getSelectedDataset());

    if (data.rows >= ENRICHR_SUBMIT_LIMIT) {
      throw new Error('Invalid amount of rows are selected (0 <= n <= 10000)');
    }

    var parsedData = parseData(data, options.form.getValue('column_with_gene_symbols'),
      options.form.getValue('ambiguous_genes_strategy'))
      .join('\n');

    if (_.size(parsedData) === 0 || _.size(parsedData) >= ENRICHR_SUBMIT_LIMIT) {
      throw new Error('Invalid amount of rows are selected (0 <= n <= 10000). Currently selected: ' + _.size(parsedData) + ' genes');
    }

    var promise = $.Deferred();

    var formData = new FormData();
    formData.append('list', parsedData);
    formData.append('description', options.project.getSelectedDataset().getName());

    $.ajax({
      type: "POST",
      url: ENRICHR_URL,
      data: formData,
      cache: false,
      contentType: false,
      processData: false,
      success: function (data) {
        data = JSON.parse(data);
        window.open('https://amp.pharm.mssm.edu/Enrichr/enrich?dataset=' + data.shortId, '_blank');
        promise.resolve();
      },
      error: function (_, __, error) {
        promise.reject();
        throw new Error(error);
      }
    });

    return promise;
  }
};

phantasus.gseaTool = function (project) {
  var self = this;

  var fullDataset = project.getFullDataset();
  var numberFields = phantasus.MetadataUtil.getMetadataSignedNumericFields(fullDataset
    .getRowMetadata());

  if (numberFields.length === 0) {
    throw new Error('No number fields available. Cannot rank by');
  }

  var rows = numberFields.map(function (field) {
    return field.getName();
  });

  this.$dialog = $('<div style="background:white;" title="gsea plot tool"><h4>Please select rows.</h4></div>');
  this.$el = $('<div class="container-fluid">'
    + '<div class="row">'
    + '<div data-name="configPane" class="col-xs-2"></div>'
    + '<div class="col-xs-10"><div style="position:relative;" data-name="chartDiv"></div></div>'
    + '</div></div>');

  var $notifyRow = this.$dialog.find('h4');

  this.formBuilder = new phantasus.FormBuilder({
    formStyle: 'vertical'
  });

  [{
    name: 'rank_by',
    options: rows,
    value: _.first(rows),
    type: 'select'
  }/*, {
    name: 'chart_width',
    type: 'range',
    value: 400,
    min: 60,
    max: 800,
    step: 10
  }, {
    name: 'chart_height',
    type: 'range',
    value: 400,
    min: 60,
    max: 800,
    step: 10
  }*/].forEach(function (a) {
    self.formBuilder.append(a);
  });

  var onChange = _.debounce(function (e) {
    var selectedDataset = project.getSelectedDataset();
    var fullDataset = project.getSortedFilteredDataset();
    $notifyRow.toggle(selectedDataset.getRowCount() === fullDataset.getRowCount());

    if (selectedDataset.getRowCount() === fullDataset.getRowCount()) {
      return;
    }

    if (self.promise) {
      self.promise.reject('Cancelled');
    }

    self.request(project).then(self.draw.bind(self), function (e) {
      self.$chart.empty();
    });
  }, 500);

  this.formBuilder.$form.on('change', 'select', onChange);
  project.getRowSelectionModel().on('selectionChanged.chart', onChange);


  var $configPane = this.$el.find('[data-name=configPane]');
  this.formBuilder.$form.appendTo($configPane);
  this.$el.appendTo(this.$dialog);
  this.$chart = this.$el.find("[data-name=chartDiv]");
  this.$dialog.dialog({
    close: function (event, ui) {
      project.getRowSelectionModel().off("selectionChanged.chart", onChange);
      self.$dialog.dialog('destroy').remove();
      event.stopPropagation();
    },

    resizable: true,
    height: 600,
    width: 900
  });

  onChange();
};

phantasus.gseaTool.prototype = {
  toString: function () {
    return 'gsea Plot';
  },
  request: function (project) {
    this.$chart.empty();
    phantasus.Util.createLoadingEl().appendTo(this.$chart);
    this.promise = $.Deferred();

    var selectedDataset = project.getSelectedDataset();
    var fullDataset = project.getSortedFilteredDataset();

    if (selectedDataset.getRowCount() === fullDataset.getRowCount()) {
      this.promise.reject('Invalid rows');
      // throw new Error('Invalid amount of rows are selected (zero rows or whole dataset selected)');
        return this.promise;
    }

    var idxs = selectedDataset.rowIndices.map(function (idx) {
      return idx + 1; // #' @param selectedGenes indexes of selected genes (starting from one, in the order of fData)
    });

    var self = this;
    var rankBy = this.formBuilder.getValue('rank_by');
    var rows = phantasus.Dataset.toJSON(fullDataset).rowMetadataModel.vectors;
    rows = rows.filter(function(row) { return row.name === rankBy; });

    var fvarLabels = rows.map(function (row) {
      return row.name
    });
    var fData = rows.reduce(function (acc, currentRow) {
      acc[currentRow.name] = currentRow.array;
      return acc;
    }, {});

    var height = 2;//this.formBuilder.getValue('chart_height');
    var width = 2;//this.formBuilder.getValue('chart_width');


    var req = ocpu.call('gseaPlot', {
      fData: fData,
      fvarLabels: fvarLabels,
      rankBy: rankBy,
      selectedGenes: idxs,
      width: width,
      height: height
    }, function (session) {
      session.getObject(function (filenames) {
        var svgPath = JSON.parse(filenames)[0];
        var absolutePath = phantasus.Util.getFilePath(session, svgPath);
        phantasus.BlobFromPath.getFileObject(absolutePath, function (blob) {
          self.imageURL = URL.createObjectURL(blob);
          self.promise.resolve(self.imageURL);
        });
      });
    }).fail(function () {
      self.promise.reject();
    });

    return self.promise;
  },
  draw: function (url) {
    this.$chart.empty();
    var svg = $('<img src="' + url + '">');
    svg.appendTo(this.$chart);
  }
};

phantasus.HClusterTool = function () {
};
phantasus.HClusterTool.PRECOMPUTED_DIST = 'Matrix values (for a precomputed distance matrix)';
phantasus.HClusterTool.PRECOMPUTED_SIM = 'Matrix values (for a precomputed similarity matrix)';
phantasus.HClusterTool.Functions = [phantasus.Euclidean, phantasus.Jaccard,
  new phantasus.OneMinusFunction(phantasus.Cosine),
  new phantasus.OneMinusFunction(phantasus.KendallsCorrelation),
  new phantasus.OneMinusFunction(phantasus.Pearson),
  new phantasus.OneMinusFunction(phantasus.Spearman),
  phantasus.HClusterTool.PRECOMPUTED_DIST,
  phantasus.HClusterTool.PRECOMPUTED_SIM];
phantasus.HClusterTool.Functions.fromString = function (s) {
  for (var i = 0; i < phantasus.HClusterTool.Functions.length; i++) {
    if (phantasus.HClusterTool.Functions[i].toString() === s) {
      return phantasus.HClusterTool.Functions[i];
    }
  }
  throw new Error(s + ' not found');
};

phantasus.HClusterTool.createLinkageMethod = function (linkageString) {
  var linkageMethod;
  if (linkageString === 'Average') {
    linkageMethod = phantasus.AverageLinkage;
  } else if (linkageString === 'Complete') {
    linkageMethod = phantasus.CompleteLinkage;
  } else if (linkageString === 'Single') {
    linkageMethod = phantasus.SingleLinkage;
  } else {
    throw new Error('Unknown linkage method ' + linkageString);
  }
  return linkageMethod;
};

phantasus.HClusterTool.execute = function (dataset, input) {
  // note: in worker here
  var linkageMethod = phantasus.HClusterTool
    .createLinkageMethod(input.linkage_method);
  var f = phantasus.HClusterTool.Functions.fromString(input.metric);
  if (f === phantasus.HClusterTool.PRECOMPUTED_DIST) {
    f = 0;
  } else if (f === phantasus.HClusterTool.PRECOMPUTED_SIM) {
    f = 1;
  }
  var rows = input.cluster == 'Rows' || input.cluster == 'Rows and columns';
  var columns = input.cluster == 'Columns'
    || input.cluster == 'Rows and columns';
  var doCluster = function (d, groupByFields) {
    return (groupByFields && groupByFields.length > 0) ? new phantasus.HClusterGroupBy(
      d, groupByFields, f, linkageMethod)
      : new phantasus.HCluster(phantasus.HCluster
      .computeDistanceMatrix(d, f), linkageMethod);
  };

  var rowsHcl;
  var columnsHcl;

  if (rows) {
    rowsHcl = doCluster(
      input.selectedColumnsToUseForClusteringRows ? new phantasus.SlicedDatasetView(dataset,
        null, input.selectedColumnsToUseForClusteringRows) : dataset,
      input.group_rows_by);
  }
  if (columns) {
    columnsHcl = doCluster(
      phantasus.DatasetUtil
        .transposedView(input.selectedRowsToUseForClusteringColumns ? new phantasus.SlicedDatasetView(
          dataset, input.selectedRowsToUseForClusteringColumns, null)
          : dataset), input.group_columns_by);

  }
  return {
    rowsHcl: rowsHcl,
    columnsHcl: columnsHcl
  };
};
phantasus.HClusterTool.prototype = {
  toString: function () {
    return 'Hierarchical Clustering';
  },
  init: function (project, form) {
    form.setOptions('group_rows_by', phantasus.MetadataUtil
      .getMetadataNames(project.getFullDataset().getRowMetadata()));
    form
      .setOptions('group_columns_by', phantasus.MetadataUtil
        .getMetadataNames(project.getFullDataset()
          .getColumnMetadata()));
    form.setVisible('group_rows_by', false);
    form
      .setVisible('cluster_rows_in_space_of_selected_columns_only',
        false);
    form.$form.find('[name=cluster]').on(
      'change',
      function (e) {
        var val = $(this).val();
        var showGroupColumns = false;
        var showGroupRows = false;
        if (val === 'Columns') {
          showGroupColumns = true;
        } else if (val === 'Rows') {
          showGroupRows = true;
        } else {
          showGroupColumns = true;
          showGroupRows = true;
        }
        form.setVisible('group_columns_by', showGroupColumns);
        form.setVisible('group_rows_by', showGroupRows);
        form.setVisible(
          'cluster_columns_in_space_of_selected_rows_only',
          showGroupColumns);
        form.setVisible(
          'cluster_rows_in_space_of_selected_columns_only',
          showGroupRows);
      });
  },
  gui: function () {
    return [{
      name: 'metric',
      options: phantasus.HClusterTool.Functions,
      value: phantasus.HClusterTool.Functions[4].toString(),
      type: 'select'
    }, {
      name: 'linkage_method',
      options: ['Average', 'Complete', 'Single'],
      value: 'Average',
      type: 'select'
    }, {
      name: 'cluster',
      options: ['Columns', 'Rows', 'Rows and columns'],
      value: 'Columns',
      type: 'select'
    }, {
      name: 'group_columns_by',
      options: [],
      type: 'bootstrap-select',
      multiple: true
    }, {
      name: 'group_rows_by',
      options: [],
      type: 'bootstrap-select',
      multiple: true
    }, {
      name: 'cluster_columns_in_space_of_selected_rows_only',
      type: 'checkbox'
    }, {
      name: 'cluster_rows_in_space_of_selected_columns_only',
      type: 'checkbox'
    }];
  },
  execute: function (options) {
    var project = options.project;
    var heatmap = options.heatMap;
    var selectedRowsToUseForClusteringColumns = options.input.cluster_columns_in_space_of_selected_rows_only ? project
      .getRowSelectionModel().getViewIndices().values()
      : null;
    if (selectedRowsToUseForClusteringColumns != null && selectedRowsToUseForClusteringColumns.length === 0) {
      selectedRowsToUseForClusteringColumns = null;
    }
    var selectedColumnsToUseForClusteringRows = options.input.cluster_rows_in_space_of_selected_columns_only ? project
      .getColumnSelectionModel().getViewIndices().values()
      : null;
    if (selectedColumnsToUseForClusteringRows != null && selectedColumnsToUseForClusteringRows.length === 0) {
      selectedColumnsToUseForClusteringRows = null;
    }
    var rows = options.input.cluster == 'Rows'
      || options.input.cluster == 'Rows and columns';
    var columns = options.input.cluster == 'Columns'
      || options.input.cluster == 'Rows and columns';
    options.input.selectedRowsToUseForClusteringColumns = selectedRowsToUseForClusteringColumns;
    options.input.selectedColumnsToUseForClusteringRows = selectedColumnsToUseForClusteringRows;
    var dataset = project.getSortedFilteredDataset();
    if (options.input.background === undefined) {
      options.input.background = true;
    }
    options.input.background = options.input.background && typeof Worker !== 'undefined';
    var rowModelOrder;
    var columnModelOrder;
    if (rows) {
      rowModelOrder = [];
      for (var i = 0; i < dataset.getRowCount(); i++) {
        rowModelOrder[i] = project.convertViewRowIndexToModel(i);
      }
    }
    if (columns) {
      columnModelOrder = [];
      for (var i = 0; i < dataset.getColumnCount(); i++) {
        columnModelOrder[i] = project.convertViewColumnIndexToModel(i);
      }
    }
    if (options.input.background === false) {
      var result = phantasus.HClusterTool.execute(dataset, options.input);
      if (result.rowsHcl) {
        var modelOrder = [];
        for (var i = 0; i < result.rowsHcl.reorderedIndices.length; i++) {
          modelOrder[i] = rowModelOrder[result.rowsHcl.reorderedIndices[i]];
        }
        heatmap.setDendrogram(result.rowsHcl.tree, false,
          modelOrder);
      }
      if (result.columnsHcl) {
        var modelOrder = [];
        for (var i = 0; i < result.columnsHcl.reorderedIndices.length; i++) {
          modelOrder[i] = columnModelOrder[result.columnsHcl.reorderedIndices[i]];
        }
        heatmap.setDendrogram(result.columnsHcl.tree, true, modelOrder);
      }
    } else {


      var blob = new Blob(
        ['self.onmessage = function(e) {'
        + 'importScripts(e.data.scripts);'
        + 'self.postMessage(phantasus.HClusterTool.execute(phantasus.Dataset.fromJSON(e.data.dataset), e.data.input));'
        + '}']);

      var url = window.URL.createObjectURL(blob);
      var worker = new Worker(url);

      worker.postMessage({
        scripts: phantasus.Util.getScriptPath(),
        dataset: phantasus.Dataset.toJSON(dataset, {
          columnFields: options.input.group_columns_by || [],
          rowFields: options.input.group_rows_by || [],
          seriesIndices: [0]
        }),
        input: options.input
      });

      worker.onmessage = function (e) {
        var result = e.data;
        if (result.rowsHcl) {
          var modelOrder = [];
          for (var i = 0; i < result.rowsHcl.reorderedIndices.length; i++) {
            modelOrder[i] = rowModelOrder[result.rowsHcl.reorderedIndices[i]];
          }
          heatmap.setDendrogram(result.rowsHcl.tree, false,
            modelOrder);
        }
        if (result.columnsHcl) {
          var modelOrder = [];
          for (var i = 0; i < result.columnsHcl.reorderedIndices.length; i++) {
            modelOrder[i] = columnModelOrder[result.columnsHcl.reorderedIndices[i]];
          }
          heatmap.setDendrogram(result.columnsHcl.tree, true,
            modelOrder);
        }
        worker.terminate();
        window.URL.revokeObjectURL(url);
      };
      return worker;
    }

  }
};

/**
 * Created by dzenkova on 11/18/16.
 */
phantasus.KmeansTool = function () {
};
phantasus.KmeansTool.prototype = {
  toString: function () {
    return "k-means";
  },
  gui: function () {
    // z-score, robust z-score, log2, inverse log2
    return [{
      name: "number_of_clusters",
      type: "text"
    }];
  },
  execute: function (options) {
    var project = options.project;
    var dataset = project.getSortedFilteredDataset();
    var promise = $.Deferred();

    var trueIndices = phantasus.Util.getTrueIndices(dataset);

    console.log(trueIndices, project.rowSelectionModel, project.columnSelectionModel);

    var columnIndices = [];
    var rowIndices = [];

    if (options.input.use_selected_only) {
      var selectedDataset = project.getSelectedDataset();
      var selectedIndices = phantasus.Util.getTrueIndices(selectedDataset);
      columnIndices = selectedIndices.columns.length > 0 ? selectedIndices.columns : trueIndices.columns;
      rowIndices = selectedIndices.rows.length > 0 ? selectedIndices.rows : trueIndices.rows;
    }
    else {
      columnIndices = trueIndices.columns;
      rowIndices = trueIndices.rows;
    }

    var number = parseInt(options.input.number_of_clusters);
    if (isNaN(number)) {
      throw new Error("Enter the expected number of clusters");
    }
    var replacena = "mean";
    var esPromise = dataset.getESSession();

    console.log(dataset);

    esPromise.then(function (essession) {
      var args = {
        es: essession,
        k: number,
        replacena: replacena
      };
      if (columnIndices.length > 0) {
        args.columns = columnIndices;
      }
      if (rowIndices.length > 0) {
        args.rows = rowIndices;
      }
      var req = ocpu.call("performKmeans", args, function (session) {
        session.getMessages(function (messages) {
          console.log("kmeans::", messages);
        });
        session.getObject(function (success) {
          var clusters = JSON.parse(success);

          var v = dataset.getRowMetadata().getByName("clusters");
          if (v == null) {
            v = dataset.getRowMetadata().add("clusters");
          }
          for (var i = 0; i < dataset.getRowCount(); i++) {
            v.setValue(i, clusters[i]);
          }
          v.getProperties().set("phantasus.dataType", "string");
          promise.resolve();
          project.trigger("trackChanged", {
            vectors: [v],
            display: ["color"]
          });
        })
      }, false, "::" + dataset.getESVariable());
      req.fail(function () {
        promise.reject();
        throw new Error("Kmeans call to OpenCPU failed" + req.responseText);
      });

    });

    return promise;
  }
};

phantasus.LimmaTool = function () {
};
phantasus.LimmaTool.prototype = {
  toString: function () {
    return "limma";
  },
  init: function (project, form) {
    var _this = this;
    var updateAB = function (fieldNames) {
      var ids = [];
      if (fieldNames != null) {
        var vectors = phantasus.MetadataUtil.getVectors(project
          .getFullDataset().getColumnMetadata(), fieldNames);
        var idToIndices = phantasus.VectorUtil
          .createValuesToIndicesMap(vectors);
        idToIndices.forEach(function (indices, id) {
          ids.push(id);
        });
      }
      ids.sort();
      form.setOptions("class_a", ids);
      form.setOptions("class_b", ids);
    };
    var $field = form.$form.find("[name=field]");
    $field.on("change", function (e) {
      updateAB($(this).val());
    });
    if ($field[0].options.length > 0) {
      $field.val($field[0].options[0].value);
    }
    updateAB($field.val());
  },
  gui: function (project) {
    var dataset = project.getSortedFilteredDataset();
    var fields = phantasus.MetadataUtil.getMetadataNames(dataset
      .getColumnMetadata());
    return [{
      name: "field",
      options: fields,
      type: "select",
      multiple: true
    }, {
      name: "class_a",
      title: "Class A",
      options: [],
      value: "",
      type: "checkbox-list",
      multiple: true
    }, {
      name: "class_b",
      title: "Class B",
      options: [],
      value: "",
      type: "checkbox-list",
      multiple: true
    }];
  },
  execute: function (options) {
    var project = options.project;
    var field = options.input.field;
    var classA = options.input.class_a;
    var classB = options.input.class_b;
    var promise = $.Deferred();

    if (classA.length == 0 || classB.length == 0) {
      throw new Error("You must choose at least one option in each class");
    }

    var dataset = project.getSortedFilteredDataset();
    // console.log(dataset);
    var es = dataset.getESSession();

    var v = dataset.getColumnMetadata().getByName("Comparison");
    if (v == null) {
      v = dataset.getColumnMetadata().add("Comparison");
    }
    var vs = [];
    field.forEach(function (name) {
      vs.push(dataset.getColumnMetadata().getByName(name));
    });

    var checkCortege = function (vectors, curClass, curColumn) {
      var columnInClass = false;
      var isEqual = true;
      for (j = 0; j < curClass.length; j += 1) {
        isEqual = true;
        for (var k = 0; k < vectors.length; k += 1) {
          isEqual &= vectors[k].getValue(curColumn) == curClass[j].array[k];
        }
        columnInClass |= isEqual;
      }
      return columnInClass;
    };
    for (var i = 0; i < dataset.getColumnCount(); i++) {
      var columnInA = checkCortege(vs, classA, i);
      var columnInB = checkCortege(vs, classB, i);
      if (columnInA && columnInB) {
        var warning = "Chosen classes have intersection in column " + i;
        throw new Error(warning);
      }
      v.setValue(i, columnInA ? "A" : (columnInB ? "B" : ""));
    }

    project.trigger("trackChanged", {
      vectors: [v],
      display: ["color"],
      columns: true
    });

    var values = Array.apply(null, Array(project.getFullDataset().getColumnCount()))
      .map(String.prototype.valueOf, "");

    for (var j = 0; j < dataset.getColumnCount(); j++) {
      values[j] = v.getValue(j);
    }

    var trueIndices = phantasus.Util.getTrueIndices(dataset);

    es.then(function (essession) {
      var args = {
        es: essession,
        fieldValues: values
      };
      if (trueIndices.rows.length > 0) {
        args.rows = trueIndices.rows;
      }
      if (trueIndices.columns.length > 0) {
        args.columns = trueIndices.columns;
      }

      var req = ocpu.call("limmaAnalysis", args, function (session) {
        session.getObject(function (success) {
          var r = new FileReader();
          var filePath = phantasus.Util.getFilePath(session, JSON.parse(success)[0]);

          r.onload = function (e) {
            var contents = e.target.result;
            var ProtoBuf = dcodeIO.ProtoBuf;
            ProtoBuf.protoFromFile("./message.proto", function (error, success) {
              if (error) {
                alert(error);
              }
              var builder = success,
                rexp = builder.build("rexp"),
                REXP = rexp.REXP,
                rclass = REXP.RClass;
              var res = REXP.decode(contents);
              var data = phantasus.Util.getRexpData(res, rclass);
              var names = phantasus.Util.getFieldNames(res, rclass);
              var vs = [];
              var rows = trueIndices.rows.length > 0 ? trueIndices.rows : dataset.rowIndices;

              names.forEach(function (name) {
                if (name !== "symbol") {
                  var v = dataset.getRowMetadata().add(name);
                  for (var i = 0; i < dataset.getRowCount(); i++) {
                    v.setValue(i, data[name].values[i]);
                  }
                  vs.push(v);
                }

              });
              // alert("Limma finished successfully");
              project.trigger("trackChanged", {
                vectors: vs,
                display: []
              });
              promise.resolve();
            })
          };
          phantasus.BlobFromPath.getFileObject(filePath, function (file) {
            r.readAsArrayBuffer(file);
          });
        })
      }, false, "::" + dataset.getESVariable());
      req.fail(function () {
        promise.reject();
        throw new Error("Limma call failed" + req.responseText);
      });
    });

    return promise;
  }
};

phantasus.MarkerSelection = function () {

};

/**
 * @private
 */
phantasus.MarkerSelection.Functions = [
  phantasus.FisherExact,
  phantasus.FoldChange, phantasus.MeanDifference, phantasus.SignalToNoise,
  phantasus.createSignalToNoiseAdjust(), phantasus.TTest];

phantasus.MarkerSelection.Functions.fromString = function (s) {
  for (var i = 0; i < phantasus.MarkerSelection.Functions.length; i++) {
    if (phantasus.MarkerSelection.Functions[i].toString() === s) {
      return phantasus.MarkerSelection.Functions[i];
    }
  }
  throw s + ' not found';
};
phantasus.MarkerSelection.execute = function (dataset, input) {
  var aIndices = [];
  var bIndices = [];
  for (var i = 0; i < input.numClassA; i++) {
    aIndices[i] = i;
  }
  for (var i = input.numClassA; i < dataset.getColumnCount(); i++) {
    bIndices[i] = i;
  }

  var f = phantasus.MarkerSelection.Functions.fromString(input.metric);
  var permutations = new phantasus.PermutationPValues(dataset, aIndices,
    bIndices, input.npermutations, f);
  return {
    rowSpecificPValues: permutations.rowSpecificPValues,
    k: permutations.k,
    fdr: permutations.fdr,
    scores: permutations.scores
  };
};
phantasus.MarkerSelection.prototype = {
  toString: function () {
    return 'Marker Selection';
  },
  init: function (project, form) {
    var _this = this;
    var updateAB = function (fieldNames) {
      var ids = [];
      if (fieldNames != null) {
        var vectors = phantasus.MetadataUtil.getVectors(project
          .getFullDataset().getColumnMetadata(), fieldNames);
        var idToIndices = phantasus.VectorUtil
          .createValuesToIndicesMap(vectors);
        idToIndices.forEach(function (indices, id) {
          ids.push(id);
        });
      }
      ids.sort();
      form.setOptions('class_a', ids);
      form.setOptions('class_b', ids);

    };
    var $field = form.$form.find('[name=field]');
    $field.on('change', function (e) {
      updateAB($(this).val());
    });

    if ($field[0].options.length > 0) {
      $field.val($field[0].options[0].value);
    }
    updateAB($field.val());
    var $metric = form.$form.find('[name=metric]');
    $metric.on('change', function (e) {
      var isFishy = $(this).val() === 'Fisher Exact Test';
      form.setVisible('grouping_value', isFishy);
      form.setVisible('permutations', !isFishy);
      form.setVisible('number_of_markers', !isFishy);

    });
    form.setVisible('grouping_value', false);

  },
  gui: function (project) {
    var dataset = project.getSortedFilteredDataset();
    var fields = phantasus.MetadataUtil.getMetadataNames(dataset
      .getColumnMetadata());
    return [
      {
        name: 'metric',
        options: phantasus.MarkerSelection.Functions,
        value: phantasus.SignalToNoise.toString(),
        type: 'select',
        help: ''
      },
      {
        name: 'grouping_value',
        value: '1',
        help: 'Class values are categorized into two groups based on whether dataset values are greater than or equal to this value'
      },
      {
        name: 'field',
        options: fields,
        type: 'select',
        multiple: true
      },
      {
        name: 'class_a',
        title: 'Class A',
        options: [],
        value: '',
        type: 'checkbox-list',
        multiple: true
      },
      {
        name: 'class_b',
        title: 'Class B',
        options: [],
        value: '',
        type: 'checkbox-list',
        multiple: true
      },
      {
        name: 'number_of_markers',
        value: '100',
        type: 'text',
        help: 'The initial number of markers to show in each direction.'
      }, {
        name: 'permutations',
        value: '0',
        type: 'text'
      }];
  },
  execute: function (options) {

    var project = options.project;
    // classA and classB are arrays of Identifiers if run via user
    // interface. If run via JSON, will be string arrays
    var classA = options.input.class_a;
    var classB = options.input.class_b;
    if (classA.length === 0 && classB.length === 0) {
      throw 'No samples in class A and class B';
    }

    if (classA.length === 0) {
      throw 'No samples in class A';
    }
    if (classB.length === 0) {
      throw 'No samples in class B';
    }
    for (var i = 0; i < classA.length; i++) {
      var val = classA[i];
      if (!(val instanceof phantasus.Identifier)) {
        classA[i] = new phantasus.Identifier(
          phantasus.Util.isArray(val) ? val : [val]);
      }
    }

    for (var i = 0; i < classB.length; i++) {
      var val = classB[i];
      if (!(val instanceof phantasus.Identifier)) {
        classB[i] = new phantasus.Identifier(
          phantasus.Util.isArray(val) ? val : [val]);
      }
    }
    var npermutations = parseInt(options.input.permutations);
    var dataset = project.getSortedFilteredDataset();

    var fieldNames = options.input.field;
    if (!phantasus.Util.isArray(fieldNames)) {
      fieldNames = [fieldNames];
    }

    var vectors = phantasus.MetadataUtil.getVectors(dataset
      .getColumnMetadata(), fieldNames);
    var idToIndices = phantasus.VectorUtil.createValuesToIndicesMap(vectors);
    var aIndices = [];
    var bIndices = [];
    classA.forEach(function (id) {
      var indices = idToIndices.get(id);
      if (indices === undefined) {
        throw new Error(id + ' not found in ' + idToIndices.keys());
      }
      aIndices = aIndices.concat(indices);
    });
    classB.forEach(function (id) {
      var indices = idToIndices.get(id);
      if (indices === undefined) {
        throw new Error(id + ' not found in ' + idToIndices.keys());
      }
      bIndices = bIndices.concat(indices);
    });

    var f = phantasus.MarkerSelection.Functions
      .fromString(options.input.metric);

    var classASet = {};
    for (var i = 0; i < aIndices.length; i++) {
      classASet[aIndices[i]] = true;
    }
    for (var i = 0; i < bIndices.length; i++) {
      if (classASet[bIndices[i]]) {
        throw 'The sample was found in class A and class B';
      }
    }
    var isFishy = f.toString() === phantasus.FisherExact.toString();
    if ((aIndices.length === 1 || bIndices.length === 1)
      && !isFishy && f.toString() !== phantasus.MeanDifference.toString()) {
      f = phantasus.FoldChange;
    }
    var list1 = new phantasus.DatasetRowView(new phantasus.SlicedDatasetView(
      dataset, null, aIndices));
    var list2 = new phantasus.DatasetRowView(new phantasus.SlicedDatasetView(
      dataset, null, bIndices));
    // remove
    // other
    // marker
    // selection
    // fields
    var markerSelectionFields = phantasus.MarkerSelection.Functions.map(
      function (f) {
        return f.toString();
      }).concat(['odds_ratio', 'FDR(BH)', 'p_value']);
    markerSelectionFields.forEach(function (name) {
      var index = phantasus.MetadataUtil.indexOf(dataset.getRowMetadata(),
        name);
      if (index !== -1) {
        dataset.getRowMetadata().remove(index);
        options.heatMap.removeTrack(name, false);
      }
    });
    var v = dataset.getRowMetadata().add(f.toString());
    var vectors = [v];
    var comparisonVector = dataset.getColumnMetadata().add('Comparison');

    for (var i = 0; i < aIndices.length; i++) {
      comparisonVector.setValue(aIndices[i], 'A');
    }
    for (var i = 0; i < bIndices.length; i++) {
      comparisonVector.setValue(bIndices[i], 'B');
    }

    function done(result) {
      if (result) {
        var pvalueVector = dataset.getRowMetadata().add('p_value');
        var fdrVector = dataset.getRowMetadata().add('FDR(BH)');
        var kVector = dataset.getRowMetadata().add('k');
        for (var i = 0, size = pvalueVector.size(); i < size; i++) {
          pvalueVector.setValue(i, result.rowSpecificPValues[i]);
          fdrVector.setValue(i, result.fdr[i]);
          kVector.setValue(i, result.k[i]);
          v.setValue(i, result.scores[i]);
        }
        kVector.getProperties().set(phantasus.VectorKeys.FORMATTER, {pattern: 'i'});
        vectors.push(pvalueVector);
        vectors.push(fdrVector);
        vectors.push(kVector);
      }
      if (project.getRowFilter().getFilters().length > 0) {
        project.getRowFilter().setAnd(true, true);
      }
      var rowFilters = project.getRowFilter().getFilters();
      // remove existing top n filters
      for (var i = 0; i < rowFilters.length; i++) {
        if (rowFilters[i] instanceof phantasus.TopNFilter) {
          project.getRowFilter().remove(i, true);
          i--;
        }
      }
      if (!isFishy) {
        project.getRowFilter().add(
          new phantasus.TopNFilter(
            parseInt(options.input.number_of_markers),
            phantasus.TopNFilter.TOP_BOTTOM, vectors[0]
              .getName()), true);
      }

      project.setRowFilter(project.getRowFilter(), true);
      project.setRowSortKeys([
        new phantasus.SortKey(vectors[0].getName(),
          isFishy ? phantasus.SortKey.SortOrder.ASCENDING
            : phantasus.SortKey.SortOrder.DESCENDING)], true);
      // select samples used in comparison
      var selectedColumnIndices = new phantasus.Set();
      aIndices.forEach(function (index) {
        selectedColumnIndices.add(index);
      });
      bIndices.forEach(function (index) {
        selectedColumnIndices.add(index);
      });
      project.getColumnSelectionModel().setViewIndices(selectedColumnIndices, true);

      project.setColumnSortKeys([
        new phantasus.SortKey(comparisonVector
          .getName(), phantasus.SortKey.SortOrder.ASCENDING)], true);

      project.trigger('trackChanged', {
        vectors: vectors,
        display: vectors.map(function () {
          return 'text';
        }),
        columns: false
      });
      project.trigger('trackChanged', {
        vectors: [comparisonVector],
        display: ['color'],
        columns: true
      });
    }

    if (isFishy) {
      var groupingValue = parseFloat(options.input.grouping_value);
      var oddsRatioVector = dataset.getRowMetadata().add('odds_ratio');
      var fdrVector = dataset.getRowMetadata().add('FDR(BH)');
      var contingencyTableVector = dataset.getRowMetadata().add(
        'contingency_table');
      var pvalues = [];
      for (var i = 0, size = dataset.getRowCount(); i < size; i++) {
        var abcd = phantasus.createContingencyTable(list1.setIndex(i),
          list2.setIndex(i), groupingValue);
        contingencyTableVector.setValue(i, '[[' + abcd[0] + ', '
          + abcd[1] + '], [' + abcd[2] + ', ' + abcd[3] + ']]');
        var ratio = (abcd[0] * abcd[3]) / (abcd[1] * abcd[2]);
        if (isNaN(ratio) || ratio === Number.POSITIVE_INFINITY) {
          ratio = 0;
        }
        oddsRatioVector.setValue(i, ratio);
        v.setValue(i, phantasus.FisherExact.fisherTest(abcd[0], abcd[1],
          abcd[2], abcd[3]));
        pvalues.push(v.getValue(i));
      }
      var fdr = phantasus.FDR_BH(pvalues);
      for (var i = 0, size = dataset.getRowCount(); i < size; i++) {
        fdrVector.setValue(i, fdr[i]);
      }
      vectors.push(oddsRatioVector);
      vectors.push(fdrVector);
      vectors.push(contingencyTableVector);
      done();
    } else {
      if (npermutations > 0) {
        var subset = new phantasus.SlicedDatasetView(dataset, null,
          aIndices.concat(bIndices));

        options.input.background = options.input.background && typeof Worker !== 'undefined';
        options.input.numClassA = aIndices.length;
        options.input.npermutations = npermutations;
        if (options.input.background) {
          var blob = new Blob(
            [
              'self.onmessage = function(e) {'
              + 'importScripts(e.data.scripts);'
              + 'self.postMessage(phantasus.MarkerSelection.execute(phantasus.Dataset.fromJSON(e.data.dataset), e.data.input));'
              + '}']);

          var url = window.URL.createObjectURL(blob);
          var worker = new Worker(url);
          worker.postMessage({
            scripts: phantasus.Util.getScriptPath(),
            dataset: phantasus.Dataset.toJSON(subset, {
              columnFields: [],
              rowFields: [],
              seriesIndices: [0]
            }),
            input: options.input
          });

          worker.onmessage = function (e) {
            done(e.data);
            worker.terminate();
            window.URL.revokeObjectURL(url);
          };
          return worker;
        } else {
          done(phantasus.MarkerSelection.execute(subset, options.input));
        }
      } else {
        for (var i = 0, size = dataset.getRowCount(); i < size; i++) {
          v.setValue(i, f(list1.setIndex(i), list2.setIndex(i)));
        }
        // no permutations, compute asymptotic p-value if t-test
        if (f.toString() === phantasus.TTest.toString() && typeof jStat !== 'undefined') {
          var pvalueVector = dataset.getRowMetadata().add('p_value');
          var fdrVector = dataset.getRowMetadata().add('FDR(BH)');
          var rowSpecificPValues = new Float32Array(dataset.getRowCount());
          for (var i = 0, size = dataset.getRowCount(); i < size; i++) {
            list1.setIndex(i);
            list2.setIndex(i);
            var m1 = phantasus.Mean(list1);
            var m2 = phantasus.Mean(list2);
            var v1 = phantasus.Variance(list1, m1);
            var v2 = phantasus.Variance(list2, m2);
            var n1 = phantasus.CountNonNaN(list1);
            var n2 = phantasus.CountNonNaN(list2);
            var df = phantasus.DegreesOfFreedom(v1, v2, n1, n2);
            var t = v.getValue(i);
            var p = 2.0 * (1 - jStat.studentt.cdf(Math.abs(t), df));
            rowSpecificPValues[i] = p;
            pvalueVector.setValue(i, p);
          }
          vectors.push(pvalueVector);
          var fdr = phantasus.FDR_BH(rowSpecificPValues);
          for (var i = 0, size = dataset.getRowCount(); i < size; i++) {
            fdrVector.setValue(i, fdr[i]);
          }
          vectors.push(fdrVector);
        }

        done();
      }
    }

  }
};

phantasus.NearestNeighbors = function () {
};
phantasus.NearestNeighbors.Functions = [
  phantasus.Cosine, phantasus.Euclidean,
  phantasus.Jaccard, phantasus.KendallsCorrelation, phantasus.Pearson, phantasus.Spearman,
  phantasus.WeightedMean];
phantasus.NearestNeighbors.Functions.fromString = function (s) {
  for (var i = 0; i < phantasus.NearestNeighbors.Functions.length; i++) {
    if (phantasus.NearestNeighbors.Functions[i].toString() === s) {
      return phantasus.NearestNeighbors.Functions[i];
    }
  }
  throw new Error(s + ' not found');
};

phantasus.NearestNeighbors.execute = function (dataset, input) {
  var f = phantasus.NearestNeighbors.Functions.fromString(input.metric);
  var permutations = new phantasus.PermutationPValues(dataset, null, null, input.npermutations, f,
    phantasus.Vector.fromArray('', input.listValues));
  return {
    rowSpecificPValues: permutations.rowSpecificPValues,
    k: permutations.k,
    fdr: permutations.fdr,
    scores: permutations.scores
  };
};
phantasus.NearestNeighbors.prototype = {
  toString: function () {
    return 'Nearest Neighbors';
  },
  init: function (project, form) {
    var $selectedOnly = form.$form.find('[name=use_selected_only]').parent();
    form.$form.find('[name=compute_nearest_neighbors_of]').on(
      'change',
      function (e) {
        var val = $(this).val();
        if (val === 'selected rows' || val === 'column annotation') {
          $($selectedOnly.contents()[1]).replaceWith(
            document.createTextNode(' Use selected columns only'));
        } else {
          $($selectedOnly.contents()[1]).replaceWith(
            document.createTextNode(' Use selected rows only'));
        }
        form.setVisible('annotation', false);
        if (val === 'column annotation' || val === 'row annotation') {
          var metadata = val === 'column annotation'
            ? project.getFullDataset().getColumnMetadata()
            : project.getFullDataset().getRowMetadata();
          var names = [];
          // get numeric columns only
          for (var i = 0; i < metadata.getMetadataCount(); i++) {
            var v = metadata.get(i);
            if (phantasus.VectorUtil.getDataType(v) === 'number') {
              names.push(v.getName());
            }
          }
          names.sort(function (a, b) {
            a = a.toLowerCase();
            b = b.toLowerCase();
            return (a < b ? -1 : (a === b ? 0 : 1));
          });
          form.setOptions('annotation', names);
          form.setVisible('annotation', true);
        }
      });
    $($selectedOnly.contents()[1]).replaceWith(
      document.createTextNode(' Use selected columns only'));
    form.setVisible('annotation', false);
  },
  gui: function () {
    return [
      {
        name: 'metric',
        options: phantasus.NearestNeighbors.Functions,
        value: phantasus.Pearson.toString(),
        type: 'select'
      }, {
        name: 'compute_nearest_neighbors_of',
        options: ['selected rows', 'selected columns', 'column annotation', 'row annotation'],
        value: 'selected rows',
        type: 'radio'
      }, {
        name: 'use_selected_only',
        type: 'checkbox'
      }, {
        name: 'annotation',
        type: 'bootstrap-select'
      }, {
        name: 'permutations',
        value: '0',
        type: 'text'
      }];
  },
  execute: function (options) {
    var project = options.project;
    var isColumns = options.input.compute_nearest_neighbors_of == 'selected columns' ||
      options.input.compute_nearest_neighbors_of == 'row annotation';
    var isAnnotation = options.input.compute_nearest_neighbors_of == 'column annotation' ||
      options.input.compute_nearest_neighbors_of == 'row annotation';
    var heatMap = options.heatMap;
    var f = phantasus.NearestNeighbors.Functions.fromString(options.input.metric);
    var dataset = project.getSortedFilteredDataset();

    if (isColumns) {
      // compute the nearest neighbors of row, so need to transpose
      dataset = phantasus.DatasetUtil.transposedView(dataset);
    }
    var selectedIndices = (isColumns ? project.getColumnSelectionModel()
      : project.getRowSelectionModel()).getViewIndices().values();
    if (!isAnnotation && selectedIndices.length === 0) {
      throw new Error('No ' + (isColumns ? 'columns' : 'rows')
        + ' selected');
    }
    var spaceIndices = null;
    if (options.input.use_selected_only) {
      spaceIndices = (!isColumns ? project.getColumnSelectionModel()
        : project.getRowSelectionModel()).getViewIndices().values();
      dataset = phantasus.DatasetUtil.slicedView(dataset, null,
        spaceIndices);
    }
    var d1 = phantasus.DatasetUtil.slicedView(dataset, selectedIndices, null);
    var nearestNeighborsList;
    if (isAnnotation) {
      nearestNeighborsList = dataset.getColumnMetadata().getByName(options.input.annotation);
      if (!nearestNeighborsList) {
        throw new Error('No annotation selected.');
      }
    } else {
      if (d1.getRowCount() > 1) {
        // collapse each column in the dataset to a single value
        var columnView = new phantasus.DatasetColumnView(d1);
        var newDataset = new phantasus.Dataset({
          name: '',
          rows: 1,
          columns: d1.getColumnCount()
        });
        for (var j = 0, ncols = d1.getColumnCount(); j < ncols; j++) {
          var v = phantasus.Percentile(columnView.setIndex(j), 50);
          newDataset.setValue(0, j, v);
        }
        d1 = newDataset;
      }
      nearestNeighborsList = new phantasus.DatasetRowView(d1);
    }

    var npermutations = parseInt(options.input.permutations);
    var scoreVector = dataset.getRowMetadata().add(f.toString());
    if (npermutations > 0) {

      if (options.input.background === undefined) {
        options.input.background = true;
      }
      options.input.background = options.input.background && typeof Worker !== 'undefined';
      options.input.npermutations = npermutations;

      var done = function (result) {
        var pvalueVector = dataset.getRowMetadata().add('p_value');
        var fdrVector = dataset.getRowMetadata().add('FDR(BH)');
        var kVector = dataset.getRowMetadata().add('k');

        for (var i = 0, size = pvalueVector.size(); i < size; i++) {
          pvalueVector.setValue(i, result.rowSpecificPValues[i]);
          fdrVector.setValue(i, result.fdr[i]);
          kVector.setValue(i, result.k[i]);
          scoreVector.setValue(i, result.scores[i]);
        }
        kVector.getProperties().set(phantasus.VectorKeys.FORMATTER, {pattern: 'i'});
        var vectors = [pvalueVector, fdrVector, kVector, scoreVector];
        project.trigger('trackChanged', {
          vectors: vectors,
          display: ['text'],
          columns: isColumns
        });
      };

      var listValues = new Float32Array(nearestNeighborsList.size());
      for (var i = 0, size = listValues.length; i < size; i++) {
        listValues[i] = nearestNeighborsList.getValue(i);
      }
      options.input.listValues = listValues;
      if (options.input.background) {
        var blob = new Blob(
          [
            'self.onmessage = function(e) {'
            + 'importScripts(e.data.scripts);'
            +
            'self.postMessage(phantasus.NearestNeighbors.execute(phantasus.Dataset.fromJSON(e.data.dataset), e.data.input));'
            + '}']);

        var url = window.URL.createObjectURL(blob);
        var worker = new Worker(url);

        worker.postMessage({
          scripts: phantasus.Util.getScriptPath(),
          dataset: phantasus.Dataset.toJSON(dataset, {
            columnFields: [],
            rowFields: [],
            seriesIndices: [0]
          }),
          input: options.input
        });

        worker.onmessage = function (e) {
          done(e.data);
          worker.terminate();
          window.URL.revokeObjectURL(url);
        };
        return worker;
      } else {
        done(phantasus.NearestNeighbors.execute(dataset, options.input));
      }

    } else {
      var datasetRowView = new phantasus.DatasetRowView(dataset);

      for (var i = 0, size = dataset.getRowCount(); i < size; i++) {
        scoreVector.setValue(i, f(nearestNeighborsList, datasetRowView.setIndex(i)));
      }
      if (!isColumns) {
        project.setRowSortKeys([
          new phantasus.SortKey(f.toString(),
            phantasus.SortKey.SortOrder.DESCENDING)], true);
      } else {
        project.setColumnSortKeys([
          new phantasus.SortKey(f.toString(),
            phantasus.SortKey.SortOrder.DESCENDING)], true);
      }
      project.trigger('trackChanged', {
        vectors: [scoreVector],
        display: ['text'],
        columns: isColumns
      });
    }

  }
};

phantasus.NewHeatMapTool = function () {
};
phantasus.NewHeatMapTool.prototype = {
  toString: function () {
    return 'New Heat Map';
  },
  // gui : function() {
  // return [ {
  // name : 'name',
  // type : 'text'
  // }, {
  // name : 'include_selected_rows',
  // type : 'checkbox',
  // value : true
  // }, {
  // name : 'include_selected_columns',
  // type : 'checkbox',
  // value : true
  // } ];
  // },
  execute: function (options) {
    var project = options.project;
    var heatMap = options.heatMap;
    var dataset = project.getSelectedDataset({
      selectedRows: true,
      selectedColumns: true
    });
    phantasus.DatasetUtil.shallowCopy(dataset);
    //phantasus.DatasetUtil.toESSessionPromise(dataset);
    // console.log(dataset);
    // TODO see if we can subset dendrograms
    // only handle contiguous selections for now
    // if (heatMap.columnDendrogram != null) {
    // var indices = project.getColumnSelectionModel().getViewIndices()
    // .toArray();
    // phantasus.DendrogramUtil.leastCommonAncestor();
    // }
    // if (heatMap.rowDendrogram != null) {
    //
    // }
    var heatmap = new phantasus.HeatMap({
      name: heatMap.getName(),
      dataset: dataset,
      parent: heatMap,
      symmetric: project.isSymmetric() && dataset.getColumnCount() === dataset.getRowCount()
    });

  }
};

phantasus.OpenDatasetTool = function () {
};

phantasus.OpenDatasetTool.prototype = {
  toString: function () {
    return 'Open Dataset';
  },
  _read: function (options, deferred) {
    var _this = this;
    var project = options.project;
    var heatMap = options.heatMap;
    var file = options.input.file;
    var action = options.input.open_file_action;
    var dataset = project.getSortedFilteredDataset();
    deferred.fail(function (err) {
      var message = [
        'Error opening ' + phantasus.Util.getFileName(file)
      + '.'];
      if (err.message) {
        message.push('<br />Cause: ');
        message.push(err.message);
      }
      phantasus.FormBuilder.showInModal({
        title: 'Error',
        html: message.join(''),
        focus: document.activeElement
      });
    });
    deferred
      .done(function (newDataset) {

        var extension = phantasus.Util.getExtension(phantasus.Util
          .getFileName(file));
        var filename = phantasus.Util.getBaseFileName(phantasus.Util
          .getFileName(file));
        if (action === 'append' || action === 'append columns') {

          // "append": append rows to current dataset
          var appendRows = action === 'append';
          // rename fields?
          _.each(heatMap.options.rows, function (item) {
            if (item.renameTo) {
              var v = newDataset.getRowMetadata().getByName(
                item.field);
              if (v) {
                v.setName(item.renameTo);
              }
            }
          });
          _.each(heatMap.options.columns, function (item) {
            if (item.renameTo) {
              var v = newDataset.getColumnMetadata()
                .getByName(item.field);
              if (v) {
                v.setName(item.renameTo);
              }
            }
          });

          if (heatMap.options.datasetReady) {
            heatMap.options.datasetReady(newDataset);
          }
          var currentDatasetMetadataNames = phantasus.MetadataUtil
            .getMetadataNames(!appendRows ? dataset
              .getRowMetadata() : dataset
              .getColumnMetadata());
          var newDatasetMetadataNames = phantasus.MetadataUtil
            .getMetadataNames(!appendRows ? newDataset
              .getRowMetadata() : newDataset
              .getColumnMetadata());

          if (currentDatasetMetadataNames.length > 1
            || newDatasetMetadataNames.length > 1) {

            _this
              ._matchAppend(
                newDatasetMetadataNames,
                currentDatasetMetadataNames,
                heatMap,
                function (appendOptions) {
                  heatMap
                    .getProject()
                    .setFullDataset(
                      appendRows ? new phantasus.JoinedDataset(
                        dataset,
                        newDataset,
                        appendOptions.current_dataset_annotation_name,
                        appendOptions.new_dataset_annotation_name)
                        : new phantasus.TransposedDatasetView(
                        new phantasus.JoinedDataset(
                          new phantasus.TransposedDatasetView(
                            dataset),
                          new phantasus.TransposedDatasetView(
                            newDataset),
                          appendOptions.current_dataset_annotation_name,
                          appendOptions.new_dataset_annotation_name)),
                      true);

                  if (heatMap.options.renderReady) {
                    heatMap.options
                      .renderReady(heatMap);
                    heatMap.updateDataset();
                  }
                  if (appendRows) {
                    heatMap
                      .getHeatMapElementComponent()
                      .getColorScheme()
                      .setSeparateColorSchemeForRowMetadataField(
                        'Source');

                    var sourcesSet = phantasus.VectorUtil
                      .getSet(heatMap
                        .getProject()
                        .getFullDataset()
                        .getRowMetadata()
                        .getByName(
                          'Source'));
                    sourcesSet
                      .forEach(function (source) {
                        heatMap
                          .autoDisplay({
                            extension: phantasus.Util
                              .getExtension(source),
                            filename: source
                          });
                      });
                  }

                  heatMap.tabManager
                    .setTabTitle(
                      heatMap.tabId,
                      heatMap
                        .getProject()
                        .getFullDataset()
                        .getRowCount()
                      + ' row'
                      + phantasus.Util
                        .s(heatMap
                          .getProject()
                          .getFullDataset()
                          .getRowCount())
                      + ' x '
                      + heatMap
                        .getProject()
                        .getFullDataset()
                        .getColumnCount()
                      + ' column'
                      + phantasus.Util
                        .s(heatMap
                          .getProject()
                          .getFullDataset()
                          .getColumnCount()));
                  heatMap.revalidate();
                });
          } else { // no need to prompt
            heatMap
              .getProject()
              .setFullDataset(
                appendRows ? new phantasus.JoinedDataset(
                  dataset,
                  newDataset,
                  currentDatasetMetadataNames[0],
                  newDatasetMetadataNames[0])
                  : new phantasus.TransposedDatasetView(
                  new phantasus.JoinedDataset(
                    new phantasus.TransposedDatasetView(
                      dataset),
                    new phantasus.TransposedDatasetView(
                      newDataset),
                    currentDatasetMetadataNames[0],
                    newDatasetMetadataNames[0])),
                true);
            if (heatMap.options.renderReady) {
              heatMap.options.renderReady(heatMap);
              heatMap.updateDataset();
            }
            if (appendRows) {
              heatMap
                .getHeatMapElementComponent()
                .getColorScheme()
                .setSeparateColorSchemeForRowMetadataField(
                  'Source');
              var sourcesSet = phantasus.VectorUtil
                .getSet(heatMap.getProject()
                  .getFullDataset()
                  .getRowMetadata().getByName(
                    'Source'));
              sourcesSet.forEach(function (source) {
                heatMap.autoDisplay({
                  extension: phantasus.Util
                    .getExtension(source),
                  filename: source
                });
              });
            }
            heatMap.tabManager.setTabTitle(heatMap.tabId,
              heatMap.getProject().getFullDataset()
                .getRowCount()
              + ' row'
              + phantasus.Util.s(heatMap
                .getProject()
                .getFullDataset()
                .getRowCount())
              + ' x '
              + heatMap.getProject()
                .getFullDataset()
                .getColumnCount()
              + ' column'
              + phantasus.Util.s(heatMap
                .getProject()
                .getFullDataset()
                .getColumnCount()));
            heatMap.revalidate();
          }

        } else if (action === 'overlay') {
          _this
            ._matchOverlay(
              phantasus.MetadataUtil
                .getMetadataNames(newDataset
                  .getColumnMetadata()),
              phantasus.MetadataUtil
                .getMetadataNames(dataset
                  .getColumnMetadata()),
              phantasus.MetadataUtil
                .getMetadataNames(newDataset
                  .getRowMetadata()),
              phantasus.MetadataUtil
                .getMetadataNames(dataset
                  .getRowMetadata()),
              heatMap,
              function (appendOptions) {
                phantasus.DatasetUtil.overlay({
                  dataset: dataset,
                  newDataset: newDataset,
                  rowAnnotationName: appendOptions.current_dataset_row_annotation_name,
                  newRowAnnotationName: appendOptions.new_dataset_row_annotation_name,
                  columnAnnotationName: appendOptions.current_dataset_column_annotation_name,
                  newColumnAnnotationName: appendOptions.new_dataset_column_annotation_name
                });
              });
        } else if (action === 'open') { // new tab
          console.log('open')
          if (newDataset.length && newDataset.length > 0) {
            for (var i = 0; i < newDataset.length; i++) {
              new phantasus.HeatMap({
                dataset: newDataset[i],
                name: newDataset[i].seriesNames[0],
                parent: heatMap,
                inheritFromParent: false
              });
            }
          } else {
            new phantasus.HeatMap({
              dataset: newDataset,
              parent: heatMap,
              inheritFromParent: false
            });
          }
        } else {
          console.log('Unknown action: ' + action);
        }
        if (action !== 'open') {
          heatMap.revalidate();
        }
      });
  },
  execute: function (options) {
    var file = options.input.file;

    console.log("openDatasetTool.execute", file);
    var _this = this;
    var d = $.Deferred();
    phantasus.OpenDatasetTool
      .fileExtensionPrompt(file,
        function (readOptions) {
          if (!readOptions) {
            readOptions = {};
          }
          readOptions.interactive = true;
          if (options.input.isGEO) {
            readOptions.isGEO = true;
          }
          if (options.input.preloaded) {
            readOptions.preloaded = true;
          }
          var deferred = phantasus.DatasetUtil.read(file,
            readOptions);
          deferred.always(function () {
            d.resolve();
          });
          _this._read(options, deferred);
        });
    return d;
  }, // prompt for metadata field name in dataset and in file
  _matchAppend: function (newDatasetMetadataNames,
                          currentDatasetMetadataNames, heatMap, callback) {
    var tool = {};
    tool.execute = function (options) {
      return options.input;
    };
    tool.toString = function () {
      return 'Select Fields';
    };
    tool.gui = function () {
      var items = [
        {
          name: 'current_dataset_annotation_name',
          options: currentDatasetMetadataNames,
          type: 'select',
          value: 'id',
          required: true
        }];
      items.push({
        name: 'new_dataset_annotation_name',
        type: 'select',
        value: 'id',
        options: newDatasetMetadataNames,
        required: true
      });
      return items;
    };
    phantasus.HeatMap.showTool(tool, heatMap, callback);
  },
  _matchOverlay: function (newDatasetColumnMetadataNames,
                           currentDatasetColumnMetadataNames, newDatasetRowMetadataNames,
                           currentDatasetRowMetadataNames, heatMap, callback) {
    var tool = {};
    tool.execute = function (options) {
      return options.input;
    };
    tool.toString = function () {
      return 'Select Fields';
    };
    tool.gui = function () {
      var items = [];
      items.push({
        name: 'current_dataset_column_annotation_name',
        options: currentDatasetColumnMetadataNames,
        type: 'select',
        value: 'id',
        required: true
      });
      items.push({
        name: 'new_dataset_column_annotation_name',
        type: 'select',
        value: 'id',
        options: newDatasetColumnMetadataNames,
        required: true
      });
      items.push({
        name: 'current_dataset_row_annotation_name',
        options: currentDatasetRowMetadataNames,
        type: 'select',
        value: 'id',
        required: true
      });
      items.push({
        name: 'new_dataset_row_annotation_name',
        type: 'select',
        value: 'id',
        options: newDatasetRowMetadataNames,
        required: true
      });
      return items;
    };
    phantasus.HeatMap.showTool(tool, heatMap, callback);
  }
};

phantasus.OpenDatasetTool.fileExtensionPrompt = function (file, callback) {
  var ext = phantasus.Util.getExtension(phantasus.Util.getFileName(file));
  var deferred;
  if (ext === 'seg' || ext === 'segtab') {
    this._promptSegtab(function (regions) {
      callback(regions);
    });

  } else {
    callback(null);
  }

};
phantasus.OpenDatasetTool._promptMaf = function (promptCallback) {
  var formBuilder = new phantasus.FormBuilder();
  formBuilder
    .append({
      name: 'MAF_gene_symbols',
      value: '',
      type: 'textarea',
      required: true,
      help: 'Enter one gene symbol per line to filter genes. Leave blank to show all genes.'
    });
  phantasus.FormBuilder
    .showInModal({
      title: 'Gene Symbols',
      html: formBuilder.$form,
      close: 'OK',
      onClose: function () {
        var text = formBuilder.getValue('MAF_gene_symbols');
        var lines = phantasus.Util.splitOnNewLine(text);
        var mafGeneFilter = new phantasus.Map();
        for (var i = 0, nlines = lines.length, counter = 0; i < nlines; i++) {
          var line = lines[i];
          if (line !== '') {
            mafGeneFilter.set(line, counter++);
          }
        }
        var readOptions = mafGeneFilter.size() > 0 ? {
          mafGeneFilter: mafGeneFilter
        } : null;
        promptCallback(readOptions);
      }
    });
};
phantasus.OpenDatasetTool._promptSegtab = function (promptCallback) {
  var formBuilder = new phantasus.FormBuilder();
  formBuilder
    .append({
      name: 'regions',
      value: '',
      type: 'textarea',
      required: true,
      help: 'Define the regions over which you want to define the CNAs. Enter one region per line. Each line should contain region_id, chromosome, start, and end separated by a tab. Leave blank to use all unique segments in the segtab file as regions.'
    });
  phantasus.FormBuilder
    .showInModal({
      title: 'Regions',
      html: formBuilder.$form,
      close: 'OK',
      onClose: function () {
        var text = formBuilder.getValue('regions');
        var lines = phantasus.Util.splitOnNewLine(text);
        var regions = [];
        var tab = /\t/;
        for (var i = 0, nlines = lines.length, counter = 0; i < nlines; i++) {
          var line = lines[i];

          if (line !== '') {
            var tokens = line.split(tab);
            if (tokens.length >= 4) {
              regions.push({
                id: tokens[0],
                chromosome: tokens[1],
                start: parseInt(tokens[2]),
                end: parseInt(tokens[3])
              });
            }
          }
        }
        var readOptions = regions.length > 0 ? {
          regions: regions
        } : null;
        promptCallback(readOptions);
      }
    });
};

phantasus.OpenDendrogramTool = function (file) {
  this._file = file;
};
phantasus.OpenDendrogramTool.prototype = {
  toString: function () {
    return 'Open Dendrogram';
  },
  init: function (project, form) {
    var setValue = function (val) {
      var isRows = val === 'Rows';
      var names = phantasus.MetadataUtil.getMetadataNames(isRows ? project
        .getFullDataset().getRowMetadata() : project
        .getFullDataset().getColumnMetadata());
      names.unshift('Newick file does not contain node ids');
      form.setOptions('match_leaf_node_ids_to', names);
    };
    form.$form.find('[name=orientation]').on('change', function (e) {
      setValue($(this).val());
    });
    setValue('Columns');
  },
  gui: function () {
    return [{
      name: 'orientation',
      options: ['Columns', 'Rows'],
      value: 'Columns',
      type: 'radio'
    }, {
      name: 'match_leaf_node_ids_to',
      options: [],
      type: 'select'
    }];
  },
  execute: function (options) {
    var fileOrUrl = this._file;
    var isColumns = options.input.orientation === 'Columns';
    var dendrogramField = options.input.match_leaf_node_ids_to;
    if (dendrogramField == '' || dendrogramField === 'Newick file does not contain node ids') {
      dendrogramField = null;
    }
    var heatMap = options.heatMap;
    var dendrogramDeferred = phantasus.Util.getText(fileOrUrl);
    dendrogramDeferred
      .done(function (text) {
        var dataset = options.project.getSortedFilteredDataset();
        if (isColumns) {
          dataset = phantasus.DatasetUtil.transposedView(dataset);
        }
        var tree = phantasus.DendrogramUtil.parseNewick(text);
        if (tree.leafNodes.length !== dataset.getRowCount()) {
          throw new Error('# leaf nodes in dendrogram '
            + tree.leafNodes.length + ' != '
            + dataset.getRowCount());
        }
        var modelIndices = [];
        if (dendrogramField != null) {
          var vector = dataset.getRowMetadata().getByName(
            dendrogramField);
          var valueToIndex = phantasus.VectorUtil.createValueToIndexMap(vector);
          for (var i = 0, length = tree.leafNodes.length; i < length; i++) {
            var newickId = tree.leafNodes[i].name;
            var index = valueToIndex.get(newickId);
            if (index === undefined) {
              throw new Error('Unable to find dendrogram id '
                + tree.leafNodes[i].name
                + ' in annotations');
            }
            modelIndices.push(index);
          }
        } else {
          // see if leaf node ids are indices
          for (var i = 0, length = tree.leafNodes.length; i < length; i++) {
            var newickId = tree.leafNodes[i].name;
            newickId = parseInt(newickId);
            if (!isNaN(newickId)) {
              modelIndices.push(newickId);
            } else {
              break;
            }
          }

          if (modelIndices.length !== tree.leafNodes.length) {
            modelIndices = [];
            for (var i = 0, length = tree.leafNodes.length; i < length; i++) {
              modelIndices.push(i);
            }
          }
        }
        heatMap.setDendrogram(tree, isColumns, modelIndices);
      });
  }
};

phantasus.OpenFileTool = function (options) {
  this.options = options || {};
};
phantasus.OpenFileTool.prototype = {
  toString: function () {
    return 'Open' + (this.options.file != null ? (' - ' + this.options.file.name) : '');
  },
  gui: function () {
    var array = [{
      name: 'open_file_action',
      value: 'open',
      type: 'bootstrap-select',
      options: [{
        name: 'Open session',
        value: 'Open session'
      }, {divider: true}, {
        name: 'Annotate columns',
        value: 'Annotate Columns'
      }, {
        name: 'Annotate rows',
        value: 'Annotate Rows'
      }, {
        divider: true
      }, {
        name: 'Append rows to current dataset',
        value: 'append'
      }, {
        name: 'Append columns to current dataset',
        value: 'append columns'
      }, {
        name: 'Overlay onto current dataset',
        value: 'overlay'
      }, {
        name: 'Open dataset in new tab',
        value: 'open'
      }, {
        divider: true
      }, {
        name: 'Open dendrogram',
        value: 'Open dendrogram'
      }]
    }];
    if (this.options.file == null) {
      array.push({
        name: 'file',
        showLabel: false,
        placeholder: 'Open your own file',
        value: '',
        type: 'file',
        required: true,
        help: phantasus.DatasetUtil.DATASET_AND_SESSION_FILE_FORMATS
      });
    }
    array.options = {
      ok: this.options.file != null,
      size: 'modal-lg'
    };
    return array;
  },
  init: function (project, form, initOptions) {
    var $preloaded = $('<div></div>');
    form.$form.find('[name=open_file_action]').on(
      'change',
      function (e) {
        var action = $(this).val();
        if (action === 'append columns' || action === 'append'
          || action === 'open' || action === 'overlay') {
          form.setHelpText('file',
            phantasus.DatasetUtil.DATASET_FILE_FORMATS);
          $preloaded.show();
        } else if (action === 'Open dendrogram') {
          form.setHelpText('file',
            phantasus.DatasetUtil.DENDROGRAM_FILE_FORMATS);
          $preloaded.hide();
        } else if (action === 'Open session') {
          form.setHelpText('file', phantasus.DatasetUtil.SESSION_FILE_FORMAT);
          $preloaded.hide();
        } else {
          form.setHelpText('file',
            phantasus.DatasetUtil.ANNOTATION_FILE_FORMATS);
          $preloaded.hide();
        }
      });
    if (this.options.file == null) {
      var _this = this;
      var collapseId = _.uniqueId('phantasus');
      $('<h4><a role="button" data-toggle="collapse" href="#'
        + collapseId
        + '" aria-expanded="false" aria-controls="'
        + collapseId + '">Preloaded datasets</a></h4>').appendTo($preloaded);
      var $sampleDatasets = $('<div data-name="sampleData" id="' + collapseId + '" class="collapse"' +
        ' id="' + collapseId + '" style="overflow:auto;"></div>');
      $preloaded.appendTo(form.$form);
      var sampleDatasets = new phantasus.SampleDatasets({
        $el: $sampleDatasets,
        callback: function (heatMapOptions) {
          _this.options.file = heatMapOptions.dataset;
          _this.ok();
        }
      });
      $sampleDatasets.appendTo($preloaded);
    }
    form.on('change', function (e) {
      var value = e.value;
      if (value !== '' && value != null) {
        form.setValue('file', value);
        _this.options.file = value;
        _this.ok();
      }
    });

  },

  execute: function (options) {
    var _this = this;
    var isInteractive = this.options.file == null;
    var heatMap = options.heatMap;
    if (!isInteractive) {
      options.input.file = this.options.file;
    }
    if (options.input.file.isGEO) {
      options.input.isGEO = options.input.file.isGEO;
      options.input.file = options.input.file.name;
    }
    if (options.input.file.preloaded) {
      options.input.preloaded = options.input.file.preloaded;
      options.input.file = options.input.file.name;
    }
    var project = options.project;
    if (options.input.open_file_action === 'Open session') {
      return phantasus.Util.getText(options.input.file).done(function (text) {
        var options = JSON.parse(text);
        options.tabManager = heatMap.getTabManager();
        options.focus = true;
        options.inheritFromParent = false;
        options.landingPage = heatMap.options.landingPage;
        new phantasus.HeatMap(options);
      }).fail(function (err) {
        phantasus.FormBuilder.showMessageModal({
          title: 'Error',
          message: 'Unable to load session',
          focus: document.activeElement
        });
      });
    } else if (options.input.open_file_action === 'append columns'
      || options.input.open_file_action === 'append'
      || options.input.open_file_action === 'open'
      || options.input.open_file_action === 'overlay') {
      return new phantasus.OpenDatasetTool().execute(options);
    } else if (options.input.open_file_action === 'Open dendrogram') {
      phantasus.HeatMap.showTool(new phantasus.OpenDendrogramTool(
        options.input.file), options.heatMap);
    } else { // annotate rows or columns
      var d = $.Deferred();
      var isAnnotateColumns = options.input.open_file_action ==
        'Annotate Columns';
      var fileOrUrl = options.input.file;
      var dataset = project.getFullDataset();
      var fileName = phantasus.Util.getFileName(fileOrUrl);
      if (phantasus.Util.endsWith(fileName, '.cls')) {
        var result = phantasus.Util.readLines(fileOrUrl);
        result.always(function () {
          d.resolve();
        });
        result.done(function (lines) {
          _this.annotateCls(heatMap, dataset, fileName,
            isAnnotateColumns, lines);
        });
      } else if (phantasus.Util.endsWith(fileName, '.gmt')) {
        phantasus.ArrayBufferReader.getArrayBuffer(fileOrUrl, function (
          err,
          buf) {
          d.resolve();
          if (err) {
            throw new Error('Unable to read ' + fileOrUrl);
          }
          var sets = new phantasus.GmtReader().read(
            new phantasus.ArrayBufferReader(new Uint8Array(
              buf)));
          _this.promptSets(dataset, heatMap, isAnnotateColumns,
            sets, phantasus.Util.getBaseFileName(
              phantasus.Util.getFileName(fileOrUrl)));
        });

      } else {
        var result = phantasus.Util.readLines(fileOrUrl);
        result.done(function (lines) {
          _this.prompt(lines, dataset, heatMap, isAnnotateColumns);
        }).always(function () {
          d.resolve();
        });
        return d;
      }

    }
  },
  annotateCls: function (heatMap, dataset, fileName, isColumns, lines) {
    if (isColumns) {
      dataset = phantasus.DatasetUtil.transposedView(dataset);
    }
    var assignments = new phantasus.ClsReader().read(lines);
    if (assignments.length !== dataset.getRowCount()) {
      throw new Error(
        'Number of samples in cls file does not match dataset.');
    }
    var vector = dataset.getRowMetadata().add(
      phantasus.Util.getBaseFileName(fileName));
    for (var i = 0; i < assignments.length; i++) {
      vector.setValue(i, assignments[i]);
    }
    if (heatMap) {
      heatMap.getProject().trigger('trackChanged', {
        vectors: [vector],
        display: ['color'],
        columns: isColumns
      });
    }
  },

  annotateSets: function (dataset, isColumns, sets,
                          datasetMetadataName, setSourceFileName) {
    if (isColumns) {
      dataset = phantasus.DatasetUtil.transposedView(dataset);
    }
    var vector = dataset.getRowMetadata().getByName(datasetMetadataName);
    var idToIndices = phantasus.VectorUtil.createValueToIndicesMap(vector);
    var setVector = dataset.getRowMetadata().add(setSourceFileName);
    sets.forEach(function (set) {
      var name = set.name;
      var members = set.ids;
      members.forEach(function (id) {
        var indices = idToIndices.get(id);
        if (indices !== undefined) {
          for (var i = 0, nIndices = indices.length; i < nIndices; i++) {
            var array = setVector.getValue(indices[i]);
            if (array === undefined) {
              array = [];
            }
            array.push(name);
            setVector.setValue(indices[i], array);
          }
        }
      });
    });
    return setVector;
  },
  /**
   *
   * @param lines
   *            Lines of text in annotation file or null if a gmt file
   * @param dataset
   *            Current dataset
   * @param isColumns
   *            Whether annotating columns
   * @param sets
   *            Sets if a gmt file or null
   * @param metadataName
   *            The dataset metadata name to match on
   * @param fileColumnName
   *            The metadata file name to match on
   * @param fileColumnNamesToInclude
   *            An array of column names to include from the metadata file or
   *            null to include all
   * @param tranposed For text/Excel files only. If <code>true</code>, different annotations are on each row.
   */
  annotate: function (lines, dataset, isColumns, sets, metadataName,
                      fileColumnName, fileColumnNamesToInclude, transposed) {
    if (isColumns) {
      dataset = phantasus.DatasetUtil.transposedView(dataset);
    }
    var vector = dataset.getRowMetadata().getByName(metadataName);
    if (!vector) {
      throw new Error('vector ' + metadataName + ' not found.');
    }
    var fileColumnNamesToIncludeSet = null;
    if (fileColumnNamesToInclude) {
      fileColumnNamesToIncludeSet = new phantasus.Set();
      fileColumnNamesToInclude.forEach(function (name) {
        fileColumnNamesToIncludeSet.add(name);
      });
    }
    var vectors = [];
    var idToIndices = phantasus.VectorUtil.createValueToIndicesMap(vector);
    if (!lines) {
      _.each(
        sets,
        function (set) {
          var name = set.name;
          var members = set.ids;

          var v = dataset.getRowMetadata().add(name);
          vectors.push(v);
          _.each(
            members,
            function (id) {
              var indices = idToIndices.get(id);
              if (indices !== undefined) {
                for (var i = 0, nIndices = indices.length; i < nIndices; i++) {
                  v.setValue(
                    indices[i],
                    name);
                }
              }
            });
        });
    } else {
      var tab = /\t/;
      if (!transposed) {
        var header = lines[0].split(tab);
        var fileMatchOnColumnIndex = _.indexOf(header, fileColumnName);
        if (fileMatchOnColumnIndex === -1) {
          throw new Error(fileColumnName + ' not found in header:'
            + header);
        }
        var columnIndices = [];
        var nheaders = header.length;
        for (var j = 0; j < nheaders; j++) {
          var name = header[j];
          if (j === fileMatchOnColumnIndex) {
            continue;
          }
          if (fileColumnNamesToIncludeSet
            && !fileColumnNamesToIncludeSet.has(name)) {
            continue;
          }
          var v = dataset.getRowMetadata().getByName(name);
          if (!v) {
            v = dataset.getRowMetadata().add(name);
          }
          columnIndices.push(j);
          vectors.push(v);
        }
        var nheaders = columnIndices.length;
        for (var i = 1, nrows = lines.length; i < nrows; i++) {
          var line = lines[i].split(tab);
          var id = line[fileMatchOnColumnIndex];
          var indices = idToIndices.get(id);
          if (indices !== undefined) {
            var nIndices = indices.length;
            for (var j = 0; j < nheaders; j++) {
              var token = line[columnIndices[j]];
              var v = vectors[j];
              for (var r = 0; r < nIndices; r++) {
                v.setValue(indices[r], token);
              }
            }
          }
        }
      }
      else {
        // transposed
        var splitLines = [];
        var matchOnLine;
        for (var i = 0, nrows = lines.length; i < nrows; i++) {
          var line = lines[i].split(tab);
          var name = line[0];
          if (fileColumnName === name) {
            matchOnLine = line;
          } else {
            if (fileColumnNamesToIncludeSet
              && !fileColumnNamesToIncludeSet.has(name)) {
              continue;
            }
            splitLines.push(line);
            var v = dataset.getRowMetadata().getByName(name);
            if (!v) {
              v = dataset.getRowMetadata().add(name);
            }
            vectors.push(v);
          }
        }
        if (matchOnLine == null) {
          throw new Error(fileColumnName + ' not found in header.');
        }

        for (var fileColumnIndex = 1, ncols = matchOnLine.length; fileColumnIndex < ncols; fileColumnIndex++) {
          var id = matchOnLine[fileColumnIndex];
          var indices = idToIndices.get(id);
          if (indices !== undefined) {
            var nIndices = indices.length;
            for (var j = 0; j < splitLines.length; j++) {
              var token = splitLines[j][fileColumnIndex];
              var v = vectors[j];
              for (var r = 0; r < nIndices; r++) {
                v.setValue(indices[r], token);
              }
            }
          }

        }
      }
    }
    for (var i = 0; i < vectors.length; i++) {
      phantasus.VectorUtil.maybeConvertStringToNumber(vectors[i]);
    }
    return vectors;
  },
  // prompt for metadata field name in dataset
  promptSets: function (dataset, heatMap, isColumns, sets, setSourceFileName) {
    var promptTool = {};
    var _this = this;
    promptTool.execute = function (options) {
      var metadataName = options.input.dataset_field_name;
      var vector = _this.annotateSets(dataset, isColumns, sets,
        metadataName, setSourceFileName);

      heatMap.getProject().trigger('trackChanged', {
        vectors: [vector],
        display: ['text'],
        columns: isColumns
      });
    };
    promptTool.toString = function () {
      return 'Select Fields To Match On';
    };
    promptTool.gui = function () {
      return [
        {
          name: 'dataset_field_name',
          options: phantasus.MetadataUtil.getMetadataNames(
            isColumns ? dataset.getColumnMetadata() : dataset.getRowMetadata()),
          type: 'select',
          value: 'id',
          required: true
        }];

    };
    phantasus.HeatMap.showTool(promptTool, heatMap);

  },
  prompt: function (lines, dataset, heatMap, isColumns) {
    var promptTool = {};
    var _this = this;
    var header = lines != null ? lines[0].split('\t') : null;
    promptTool.execute = function (options) {
      var metadataName = options.input.dataset_field_name;
      var fileColumnName = options.input.file_field_name;
      var vectors = _this.annotate(lines, dataset, isColumns, null,
        metadataName, fileColumnName);

      var nameToIndex = new phantasus.Map();
      var display = [];
      for (var i = 0; i < vectors.length; i++) {
        display.push(isColumns ? 'color' : 'text');
        nameToIndex.set(vectors[i].getName(), i);
      }
      if (lines.colors) {
        var colorModel = isColumns
          ? heatMap.getProject().getColumnColorModel()
          : heatMap.getProject().getRowColorModel();
        lines.colors.forEach(function (item) {
          var index = nameToIndex.get(item.header);
          var vector = vectors[index];
          display[index] = 'color';
          colorModel.setMappedValue(vector, item.value, item.color);
        });
      }
      heatMap.getProject().trigger('trackChanged', {
        vectors: vectors,
        display: display,
        columns: isColumns
      });
    };
    promptTool.toString = function () {
      return 'Select Fields To Match On';
    };
    promptTool.gui = function () {
      var items = [
        {
          name: 'dataset_field_name',
          options: phantasus.MetadataUtil.getMetadataNames(
            isColumns ? dataset.getColumnMetadata() : dataset.getRowMetadata()),
          type: 'select',
          required: true
        }];
      if (lines) {
        items.push({
          name: 'file_field_name',
          type: 'select',
          options: _.map(header, function (item) {
            return {
              name: item,
              value: item
            };
          }),
          required: true
        });
      }
      return items;
    };
    phantasus.HeatMap.showTool(promptTool, heatMap);
  }
};

phantasus.PcaPlotTool = function (chartOptions) {
  var _this = this;
  this.project = chartOptions.project;
  var project = this.project;
  var drawFunction = null;


  this.$el = $('<div class="container-fluid">'
    + '<div class="row">'
    + '<div data-name="configPane" class="col-xs-2"></div>'
    + '<div class="col-xs-10"><div style="position:relative;" data-name="chartDiv"></div></div>'
    + '<div class=""'
    + '</div></div>');

  var formBuilder = new phantasus.FormBuilder({
    formStyle: 'vertical'
  });
  this.formBuilder = formBuilder;
  var rowOptions = [];
  var columnOptions = [];
  var numericRowOptions = [];
  var numericColumnOptions = [];
  var options = [];
  var numericOptions = [];
  var pcaOptions = [];
  var naOptions = [{
    name: "mean",
    value: "mean"
  }, {
    name: "median",
    value: "median"
  }];
  var updateOptions = function () {
    var dataset = project.getFullDataset();
    rowOptions = [{
      name: "(None)",
      value: ""
    }];
    columnOptions = [{
      name: "(None)",
      value: ""
    }];
    numericRowOptions = [{
      name: "(None)",
      value: ""
    }];
    numericColumnOptions = [{
      name: "(None)",
      value: ""
    }];
    options = [{
      name: "(None)",
      value: ""
    }];
    numericOptions = [{
      name: "(None)",
      value: ""
    }];
    pcaOptions = [];

    for (var i = 1; i <= _this.project.getSelectedDataset().getColumnCount(); i++) {
      pcaOptions.push({
        name: "PC" + String(i),
        value: i - 1
      });
    }


    phantasus.MetadataUtil.getMetadataNames(dataset.getRowMetadata())
      .forEach(
        function (name) {
          var dataType = phantasus.VectorUtil
            .getDataType(dataset.getRowMetadata()
              .getByName(name));
          if (dataType === "number"
            || dataType === "[number]") {
            numericRowOptions.push({
              name: name + " (row)",
              value: name
            });
          }
          rowOptions.push({
            name: name + " (row)",
            value: name
          });
        });

    phantasus.MetadataUtil.getMetadataNames(dataset.getColumnMetadata())
      .forEach(
        function (name) {
          var dataType = phantasus.VectorUtil
            .getDataType(dataset.getColumnMetadata()
              .getByName(name));
          if (dataType === "number"
            || dataType === "[number]") {
            numericColumnOptions.push({
              name: name + " (column)",
              value: name
            });
          }
          columnOptions.push({
            name: name + " (column)",
            value: name
          });
        });
  };

  updateOptions();

  formBuilder.append({
    name: "size",
    type: "bootstrap-select",
    options: numericColumnOptions
  });
  formBuilder.append({
    name: 'shape',
    type: 'bootstrap-select',
    options: columnOptions
  });
  formBuilder.append({
    name: "color",
    type: "bootstrap-select",
    options: columnOptions
  });
  formBuilder.append({
    name: "x-axis",
    type: "bootstrap-select",
    options: pcaOptions,
    value: 0
  });
  formBuilder.append({
    name: "y-axis",
    type: "bootstrap-select",
    options: pcaOptions,
    value: 1
  });
  formBuilder.append({
    name: "label",
    type: "bootstrap-select",
    options: columnOptions,
    value: columnOptions.indexOf('title') ? 'title' : null
  });
  formBuilder.append({
    name: 'visible_labels',
    type: 'bootstrap-select',
    options: ['On', 'Off'],
    value: 'On'
  });


  function setVisibility() {
    formBuilder.setOptions("color", columnOptions, true);
    formBuilder.setOptions("size", numericColumnOptions, true);
    formBuilder.setOptions("label", columnOptions, true);
  }

  this.tooltip = [];
  formBuilder.$form.find("select").on("change", function (e) {
    setVisibility();
    drawFunction();
  });
  setVisibility();

  var trackChanged = function () {
    //// console.log("track changed");
    updateOptions();
    setVisibility();
    formBuilder.setOptions("x-axis", pcaOptions, true);
    formBuilder.setOptions("y-axis", pcaOptions, true);
  };

  project.getColumnSelectionModel().on("selectionChanged.chart", trackChanged);
  project.getRowSelectionModel().on("selectionChanged.chart", trackChanged);
  project.on("trackChanged.chart", trackChanged);
  this.$chart = this.$el.find("[data-name=chartDiv]");
  var $dialog = $('<div style="background:white;" title="Chart"></div>');
  var $configPane = this.$el.find('[data-name=configPane]');
  formBuilder.$form.appendTo($configPane);
  this.$el.appendTo($dialog);
  $dialog.dialog({
    close: function (event, ui) {
      project.off('trackChanged.chart', trackChanged);
      project.getRowSelectionModel().off('selectionChanged.chart', trackChanged);
      project.getColumnSelectionModel().off('selectionChanged.chart', trackChanged);
      $dialog.dialog('destroy').remove();
      event.stopPropagation();
      _this.pca = null;
    },

    resizable: true,
    height: 620,
    width: 950
  });
  this.$dialog = $dialog;

  drawFunction = this.init();
  drawFunction();
};

phantasus.PcaPlotTool.getVectorInfo = function (value) {
  var field = value.substring(0, value.length - 2);
  var isColumns = value.substring(value.length - 2) === '_c';
  return {
    field: field,
    isColumns: isColumns
  };
};
phantasus.PcaPlotTool.prototype = {
  annotate: function (options) {
    var _this = this;
    var formBuilder = new phantasus.FormBuilder();
    formBuilder.append({
      name: 'annotation_name',
      type: 'text',
      required: true
    });
    formBuilder.append({
      name: 'annotation_value',
      type: 'text',
      required: true
    });
    phantasus.FormBuilder
      .showOkCancel({
        title: 'Annotate Selection',
        content: formBuilder.$form,
        okCallback: function () {
          var dataset = options.dataset;
          var eventData = options.eventData;
          var array = options.array;
          var value = formBuilder.getValue('annotation_value');
          var annotationName = formBuilder
            .getValue('annotation_name');
          // var annotate = formBuilder.getValue('annotate');
          var isRows = true;
          var isColumns = true;
          var existingRowVector = null;
          var rowVector = null;
          if (isRows) {
            existingRowVector = dataset.getRowMetadata()
              .getByName(annotationName);
            rowVector = dataset.getRowMetadata().add(
              annotationName);
          }
          var existingColumnVector = null;
          var columnVector = null;
          if (isColumns) {
            existingColumnVector = dataset.getColumnMetadata()
              .getByName(annotationName);
            columnVector = dataset.getColumnMetadata().add(
              annotationName);
          }

          for (var p = 0, nselected = eventData.points.length; p < nselected; p++) {
            var item = array[eventData.points[p].pointNumber];
            if (isRows) {
              if (_.isArray(item.row)) {
                item.row.forEach(function (r) {
                  rowVector.setValue(r, value);
                });

              } else {
                rowVector.setValue(item.row, value);
              }

            }
            if (isColumns) {
              columnVector.setValue(item.column, value);
            }
          }
          if (isRows) {
            phantasus.VectorUtil
              .maybeConvertStringToNumber(rowVector);
            _this.project.trigger('trackChanged', {
              vectors: [rowVector],
              display: existingRowVector != null ? []
                : [phantasus.VectorTrack.RENDER.TEXT],
              columns: false
            });
          }
          if (isColumns) {
            phantasus.VectorUtil
              .maybeConvertStringToNumber(columnVector);
            _this.project.trigger('trackChanged', {
              vectors: [columnVector],
              display: existingColumnVector != null ? []
                : [phantasus.VectorTrack.RENDER.TEXT],
              columns: true
            });
          }
        }
      });

  },
  init: function () {
    var _this = this;
    var plotlyDefaults = phantasus.PcaPlotTool.getPlotlyDefaults();
    var layout = plotlyDefaults.layout;
    var config = plotlyDefaults.config;
    var dataset = _this.project.getSortedFilteredDataset();

    return function () {
      _this.$chart.empty();

      // console.log("PCAPlot :: dataset:", dataset, "trueIndices:", phantasus.Util.getTrueIndices(dataset));

      var indices = phantasus.Util.getTrueIndices(dataset);
      var colorBy = _this.formBuilder.getValue('color');
      var sizeBy = _this.formBuilder.getValue('size');
      var shapeBy = _this.formBuilder.getValue('shape');

      var getTrueVector = function (vector) {
        while (vector && vector.indices.length == 0) {
          vector = vector.v;
        }
        return vector;
      };

      _this.colorByVector = getTrueVector(dataset.getColumnMetadata().getByName(colorBy));
      var colorByVector = _this.colorByVector;
      var sizeByVector = getTrueVector(dataset.getColumnMetadata().getByName(sizeBy));
      var shapeByVector = getTrueVector(dataset.getColumnMetadata().getByName(shapeBy));

      var pc1 = _this.formBuilder.getValue('x-axis');
      var pc2 = _this.formBuilder.getValue('y-axis');

      var label = _this.formBuilder.getValue('label');
      var textByVector = getTrueVector(dataset.getColumnMetadata().getByName(label));

      var na = 'mean';
      var color = colorByVector ? [] : null;
      var size = sizeByVector ? [] : 12;
      var shapes = shapeByVector ? [] : null;
      var text = [];
      var sizeFunction = null;
      var drawLabels = _this.formBuilder.getValue('visible_labels') === 'On';


      var data = [];
      if (sizeByVector) {
        var minMax = phantasus.VectorUtil.getMinMax(sizeByVector);
        sizeFunction = d3.scale.linear().domain(
          [minMax.min, minMax.max]).range([6, 19])
          .clamp(true);

        for (var j = 0; j < sizeByVector.indices.length; j++) {
          var sizeByValue = sizeByVector.getValue(j);
          size.push(sizeFunction(sizeByValue));
        }
      }
      if (textByVector) {
        for (var j = 0; j < textByVector.indices.length; j++) {
          text.push(textByVector.getValue(j));
        }
      }
      if (shapeByVector) {
        var allShapes = ['circle', 'square', 'diamond', 'cross', 'triangle-up', 'star', 'hexagram', 'bowtie', 'diamond-cross', 'hourglass', 'hash-open'];
        var uniqShapes = {};
        shapes = _.map(shapeByVector.indices, function (index) {
          var value = shapeByVector.getValue(index);

          if (!uniqShapes[value]) {
            uniqShapes[value] = allShapes[_.size(uniqShapes) % _.size(allShapes)];
          }

          return uniqShapes[value]
        });

        if (_.size(uniqShapes) > _.size(allShapes)) {
          phantasus.FormBuilder.showInModal({
            title: 'Warning',
            html: 'Too much factors for shapes. Repeating will occur'
          });
        }

        _.each(uniqShapes, function (shape, categoryName) {
          data.push({
            x: [1000], y: [1000],
            marker: {
              symbol: shape,
              color: '#000000',
              size: 10
            },
            name: categoryName,
            legendgroup: 'shapes',
            mode: "markers",
            text: text,
            type: "scatter",
            showlegend: true
          });
        });
      }

      if (colorByVector) {
        var uniqColors = {};
        color = _.map(colorByVector.indices, function (index) {
          var value = colorByVector.getValue(index);

          if (!uniqColors[value]) {
            uniqColors[value] = phantasus.VectorColorModel.CATEGORY_ALL[_.size(uniqColors) % 60];
          }

          return uniqColors[value]
        });

        _.each(uniqColors, function (color, categoryName) {
          data.push({
            x: [1000], y: [1000],
            marker: {
              fillcolor: color,
              color: color,
              size: 10
            },
            name: categoryName,
            legendgroup: 'colors',
            mode: "markers",
            type: "scatter",
            showlegend: true
          });
        });
      }

      data.unshift({
        marker: {
          color: color,
          size: size,
          symbol: shapes
        },
        name: " ",
        mode: "markers",
        text: text,
        type: "scatter",
        showlegend: false
      });


      var columnIndices = indices.columns;
      var rowIndices = indices.rows;

      if (columnIndices.length == 1) {
        new Error("Not enough columns (at least 2 required)");
        return;
      }

      var expressionSetPromise = dataset.getESSession();

      expressionSetPromise.then(function (essession) {
        var label_array = [];
        var anchor_array = [];
        var links, labels;

        var args = {
          es: essession,
          replacena: na
        };
        if (columnIndices && columnIndices.length > 0) {
          args.columns = columnIndices;
        }
        if (rowIndices && rowIndices.length > 0) {
          args.rows = rowIndices;
        }

        var prepareLabelData = function () {
          if (!label) {
            return;
          }

          var labelData = data.map(function (type) {
            if (!type.text) {
              return [];
            }

            var labels = type.x.map(function (x, idx) {
              return {
                x: x,
                y: type.y[idx],
                name: type.text[idx],
                r: type.marker.size
              };
            });
            type.text = null;
            return labels;
          });

          label_array = [].concat.apply([], labelData);
          anchor_array = [].concat.apply([], labelData);
        };

        var putLabels = function () {
          if (!label) {
            return;
          }

          var plot = _this.$chart.children()[0];
          var xrange = plot._fullLayout.xaxis.range;
          var yrange = plot._fullLayout.yaxis.range;
          var svg = d3.select(plot).select('.cartesianlayer .subplot .gridlayer');
          svg.selectAll(".label").data([]).exit().remove();
          svg.selectAll(".link").data([]).exit().remove();

          var tempLabels = label_array.filter(function (label) {
            return label.x >= xrange[0] && label.x <= xrange[1] && label.y >= yrange[0] && label.y <= yrange[1];
          }).map(function (label) {
            return {x: plot._fullLayout.xaxis.l2p(label.x), y: plot._fullLayout.yaxis.l2p(label.y), name: label.name};
          });

          var tempAnchors = anchor_array.filter(function (anchor) {
            return anchor.x >= xrange[0] && anchor.x <= xrange[1] && anchor.y >= yrange[0] && anchor.y <= yrange[1];
          }).map(function (anchor) {
            return {x: plot._fullLayout.xaxis.l2p(anchor.x), y: plot._fullLayout.yaxis.l2p(anchor.y), r: anchor.r};
          });

          // Draw labels
          labels = svg.selectAll(".label")
            .data(tempLabels)
            .enter()
            .append("text")
            .attr("class", "label")
            .attr('text-anchor', 'start')
            .text(function (d) {
              return d.name;
            })
            .attr("x", function (d) {
              return (d.x);
            })
            .attr("y", function (d) {
              return (d.y);
            })
            .attr("fill", "black");

          // Size of each label
          var index = 0;
          labels.each(function () {
            tempLabels[index].width = this.getBBox().width;
            tempLabels[index].height = this.getBBox().height;
            index += 1;
          });

          // Draw links
          links = svg.selectAll(".link")
            .data(tempLabels)
            .enter()
            .append("line")
            .attr("class", "link")
            .attr("x1", function (d) {
              return (d.x);
            })
            .attr("y1", function (d) {
              return (d.y);
            })
            .attr("x2", function (d) {
              return (d.x);
            })
            .attr("y2", function (d) {
              return (d.y);
            })
            .attr("stroke-width", 0.6)
            .attr("stroke", "gray");

          d3.labeler()
            .label(tempLabels)
            .anchor(tempAnchors)
            .width(plot._fullLayout._size.w)
            .height(plot._fullLayout._size.h)
            .force_bounds(true)
            .start(1000);

          labels
            .transition()
            .duration(800)
            .attr("x", function (d) {
              return (d.x);
            })
            .attr("y", function (d) {
              return (d.y);
            });

          links
            .transition()
            .duration(800)
            .attr("x2", function (d) {
              return (d.x);
            })
            .attr("y2", function (d) {
              return (d.y);
            });
        };

        var drawResult = function () {
          data[0].x = _this.pca.pca[pc1];
          data[0].y = _this.pca.pca[pc2];

          layout.margin = {
            b: 40,
            l: 60,
            t: 25,
            r: 10
          };
          var xmin = _.min(data[0].x),
              xmax = _.max(data[0].x),
              ymin = _.min(data[0].y),
              ymax = _.max(data[0].y);

          layout.xaxis = {
            title: _this.pca.xlabs[pc1],
            range: [xmin - (xmax - xmin) * 0.1, xmax + (xmax - xmin) * 0.1],
            zeroline: false
          };
          layout.yaxis = {
            title: _this.pca.xlabs[pc2],
            range: [ymin - (ymax - ymin) * 0.1, ymax + (ymax - ymin) * 0.1],
            zeroline: false
          };
          layout.showlegend = true;
          layout.config = config;
          layout.data = data;
          var $chart = $('<div></div>');
          var myPlot = $chart[0];
          $chart.appendTo(_this.$chart);


          if (drawLabels) {
            prepareLabelData();
          }

          Plotly.newPlot(myPlot, data, layout, config).then(putLabels);
          myPlot.on('plotly_afterplot', putLabels);
        };


        if (!_this.pca) {
          var req = ocpu.call("calcPCA", args, function (session) {
            session.getObject(function (success) {
              _this.pca = JSON.parse(success);
              drawResult();
            });

          }, false, "::" + dataset.getESVariable());
          req.fail(function () {
            new Error("PcaPlot call failed" + req.responseText);
          });
        } else {
          drawResult();
        }
      });


      expressionSetPromise.catch(function (reason) {
        alert("Problems occured during transforming dataset to ExpressionSet\n" + reason);
      });

    };
  }
};


phantasus.PcaPlotTool.getPlotlyDefaults = function () {
  var layout = {
    hovermode: 'closest',
    autosize: true,
    // paper_bgcolor: 'rgb(255,255,255)',
    // plot_bgcolor: 'rgb(229,229,229)',
    showlegend: false,
    margin: {
      l: 80,
      r: 10,
      t: 8, // leave space for modebar
      b: 14,
      autoexpand: true
    },
    titlefont: {
      size: 12
    },
    xaxis: {
      zeroline: false,
      titlefont: {
        size: 12
      },
      // gridcolor: 'rgb(255,255,255)',
      showgrid: true,
      //   showline: true,
      showticklabels: true,
      tickcolor: 'rgb(127,127,127)',
      ticks: 'outside'
    },
    yaxis: {
      zeroline: false,
      titlefont: {
        size: 12
      },
      // gridcolor: 'rgb(255,255,255)',
      showgrid: true,
      //   showline: true,
      showticklabels: true,
      tickcolor: 'rgb(127,127,127)',
      ticks: 'outside'
    }
  };

  // var toImage = {
  //   name: 'toImage',
  //   title: 'Download plot as a svg',
  //   icon: Icons.camera,
  //   click: function (gd) {
  //     var format = 'svg';
  //     Lib.notifier('Taking snapshot - this may take a few seconds', 'long');
  //     downloadImage(gd, {'format': format})
  //     .then(function (filename) {
  //       Lib.notifier('Snapshot succeeded - ' + filename, 'long');
  //     })
  //     .catch(function () {
  //       Lib.notifier('Sorry there was a problem downloading your snapshot!', 'long');
  //     });
  //   }
  // };
  var config = {
    modeBarButtonsToAdd: [],
    showLink: false,
    displayModeBar: true, // always show modebar
    displaylogo: false,
    staticPlot: false,
    showHints: true,
    modeBarButtonsToRemove: ['sendDataToCloud', 'zoomIn2d', 'zoomOut2d', 'hoverCompareCartesian', 'hoverClosestCartesian']
  };
  return {
    layout: layout,
    config: config
  };
};

phantasus.SaveDatasetTool = function () {
};
phantasus.SaveDatasetTool.prototype = {
  toString: function () {
    return 'Save Dataset';
  },
  init: function (project, form) {
    form.find('file_name').prop('autofocus', true).focus();
    var seriesNames = [];
    var dataset = project.getFullDataset();
    for (var i = 0, nseries = dataset.getSeriesCount(); i < nseries; i++) {
      seriesNames.push(dataset.getName(i)); // TODO check data type
    }
    form.setOptions('series', seriesNames.length > 1 ? seriesNames : null);
    form.setVisible('series', seriesNames.length > 1);
  },
  gui: function () {
    return [
      {
        name: 'file_name',
        type: 'text',
        help: '<a target="_blank" href="http://support.lincscloud.org/hc/en-us/articles/202105453-GCT-Gene-Cluster-Text-Format-">GCT 1.3</a>'
        + ' or <a target="_blank" href="http://www.broadinstitute.org/cancer/software/genepattern/gp_guides/file-formats/sections/gct">GCT 1.2</a> file name',
        required: true
      }, {
        name: 'file_format',
        type: 'radio',
        options: [{
          name: 'GCT version 1.2',
          value: '1.2'
        }, {
          name: 'GCT version 1.3',
          value: '1.3'
        }],
        value: '1.3',
      }, {
        name: 'series',
        type: 'select',
        options: [],
        required: true
      }, {
        name: 'save_selection_only',
        type: 'checkbox',
        required: true
      }];
  },
  execute: function (options) {
    var project = options.project;
    var format = options.input.file_format;
    var fileName = options.input.file_name;
    if (fileName === '') {
      fileName = 'dataset';
    }
    var series = options.input.series;
    var heatMap = options.heatMap;
    var dataset = options.input.save_selection_only ? project.getSelectedDataset() : project.getSortedFilteredDataset();
    var writer;
    if (format === '1.2') {
      writer = new phantasus.GctWriter12();
    } else if (format === '1.3') {
      writer = new phantasus.GctWriter();
    }

    if (series != null) {
      var seriesIndex = phantasus.DatasetUtil.getSeriesIndex(dataset, series);
      if (seriesIndex === -1) {
        seriesIndex = 0;
      }
      dataset = seriesIndex === 0 ? dataset : new phantasus.DatasetSeriesView(dataset, [seriesIndex]);
    }
    var ext = writer.getExtension ? writer.getExtension() : '';
    if (ext !== '' && !phantasus.Util.endsWith(fileName.toLowerCase(), '.' + ext)) {
      fileName += '.' + ext;
    }
    writer.setNumberFormat(heatMap.getHeatMapElementComponent().getDrawValuesFormat());
    var blobs = [];
    var textArray = [];
    var proxy = {
      push: function (text) {
        textArray.push(text);
        if (textArray.length === 10000) {
          var blob = new Blob([textArray.join('')], {type: 'text/plain;charset=charset=utf-8'});
          textArray = [];
          blobs.push(blob);
        }
      },
      join: function () {
        if (textArray.length > 0) {
          var blob = new Blob([textArray.join('')], {type: 'text/plain;charset=charset=utf-8'});
          blobs.push(blob);
          textArray = [];
        }

        var blob = new Blob(blobs, {type: 'text/plain;charset=charset=utf-8'});
        saveAs(blob, fileName, true);
      }
    };
    writer.write(dataset, proxy);
  }
};

phantasus.SaveImageTool = function () {

};
phantasus.SaveImageTool.prototype = {

  toString: function () {
    return 'Save Image';
  },
  init: function (project, form) {
    form.find('file_name').prop('autofocus', true).focus();
  },
  gui: function () {
    return [
      {
        name: 'file_name',
        type: 'text',
        required: true
      }, {
        name: 'format',
        type: 'radio',
        options: ['PDF', 'PNG', 'SVG'],
        value: 'PNG',
        required: true
      }];
  },
  execute: function (options) {
    var fileName = options.input.file_name;
    if (fileName === '') {
      fileName = 'image';
    }
    var format = options.input.format.toLowerCase();
    if (!phantasus.Util.endsWith(fileName.toLowerCase(), '.' + format)) {
      fileName += '.' + format;
    }
    var heatMap = options.heatMap;
    heatMap.saveImage(fileName, format);
  }
};

phantasus.SaveSessionTool = function () {
};
phantasus.SaveSessionTool.prototype = {
  toString: function () {
    return 'Save Session';
  },
  init: function (project, form) {
    form.find('file_name').prop('autofocus', true).focus();
  },
  gui: function () {
    return [
      {
        name: 'file_name',
        type: 'text',
        required: true
      }];
  },
  execute: function (options) {
    var fileName = options.input.file_name;
    if (fileName === '') {
      fileName = 'session.json';
    }
    if (!phantasus.Util.endsWith(fileName.toLowerCase(), '.json')) {
      fileName += '.json';
    }
    var heatMap = options.heatMap;
    // var options = {dataset: options.input.include_dataset};
    var options = {dataset: true};
    var json = heatMap.toJSON(options);
    var nativeArrayToArray = Array.from || function (typedArray) {
        var normalArray = Array.prototype.slice.call(typedArray);
        normalArray.length === typedArray.length;
        normalArray.constructor === Array;
      };
    var blob = new Blob([JSON.stringify(json, function (key, value) {
      if (phantasus.Util.isArray(value)) {
        return value instanceof Array ? value : nativeArrayToArray(value);
      }
      return value;
    })], {type: 'application/json;charset=charset=utf-8'});
    saveAs(blob, fileName, true);
  }
};

phantasus.shinyGamTool = function () {
};
phantasus.shinyGamTool.prototype = {
  toString: function () {
    return "Submit to Shiny GAM";
  },
  gui: function () {
    return [];
  },
  init: function (heatMap, form) {
    form.appendContent('<p>Are you sure you want to submit to Shiny GAM analysis?');

    var rows = phantasus.Dataset.toJSON(heatMap.getSortedFilteredDataset()).rowMetadataModel.vectors;
    var pValuePresent = _.size(_.where(rows, {'name': 'P.Value'})) > 0;
    if (!pValuePresent) {
      form.appendContent('<span class="phantasus-warning-color">Warning:</span>' +
        'differential expression analysis (limma) is required to be run before submitting to Shiny GAM.');
    }

    form.appendContent('</p>');
    form.appendContent('Result will open in a new window automatically. <br/>' +
      'Your browser may be irresponsive for an amount of time');
  },
  execute: function (options) {
    var rows = phantasus.Dataset.toJSON(options.project.getSortedFilteredDataset()).rowMetadataModel.vectors;
    var fvarLabels = rows.map(function (row) {
      return row.name
    });
    var fData = rows.reduce(function (acc, currentRow, index) {
      acc[currentRow.name] = currentRow.array;
      return acc;
    }, {});
    var promise = $.Deferred();

    ocpu.call('shinyGAMAnalysis', {
      fData: fData,
      fvarLabels: fvarLabels
    }, function (context) {
      context.getObject('.val', null, function (link) {
        window.open(link.split('"')[1], '_blank');
      });
      promise.resolve();
    }).fail(function () {
      console.error('Failed to submit to shiny GAM analysis. Reason: ' + req.responseText);
      promise.reject();
    });

    return promise;
  }
};

phantasus.SimilarityMatrixTool = function () {
};

phantasus.SimilarityMatrixTool.Functions = [phantasus.Euclidean,
  phantasus.Jaccard, phantasus.Cosine, phantasus.KendallsCorrelation, phantasus.Pearson, phantasus.Spearman];
phantasus.SimilarityMatrixTool.Functions.fromString = function (s) {
  for (var i = 0; i < phantasus.SimilarityMatrixTool.Functions.length; i++) {
    if (phantasus.SimilarityMatrixTool.Functions[i].toString() === s) {
      return phantasus.SimilarityMatrixTool.Functions[i];
    }
  }
  throw new Error(s + ' not found');
};
phantasus.SimilarityMatrixTool.execute = function (dataset, input) {
  var isColumnMatrix = input.compute_matrix_for == 'Columns';
  var f = phantasus.SimilarityMatrixTool.Functions.fromString(input.metric);
  return phantasus.HCluster.computeDistanceMatrix(
    isColumnMatrix ? new phantasus.TransposedDatasetView(dataset)
      : dataset, f);
};
phantasus.SimilarityMatrixTool.prototype = {
  toString: function () {
    return 'Similarity Matrix';
  },
  init: function (project, form) {

  },
  gui: function () {
    return [{
      name: 'metric',
      options: phantasus.SimilarityMatrixTool.Functions,
      value: phantasus.SimilarityMatrixTool.Functions[4].toString(),
      type: 'select'
    }, {
      name: 'compute_matrix_for',
      options: ['Columns', 'Rows'],
      value: 'Columns',
      type: 'radio'
    }];
  },
  execute: function (options) {
    var project = options.project;
    var heatMap = options.heatMap;
    var isColumnMatrix = options.input.compute_matrix_for == 'Columns';
    var f = phantasus.SimilarityMatrixTool.Functions
      .fromString(options.input.metric);
    var dataset = project.getSortedFilteredDataset();
    var blob = new Blob(
      ['self.onmessage = function(e) {'
      + 'importScripts(e.data.scripts);'
      + 'self.postMessage(phantasus.SimilarityMatrixTool.execute(phantasus.Dataset.fromJSON(e.data.dataset), e.data.input));'
      + '}']);

    var url = window.URL.createObjectURL(blob);
    var worker = new Worker(url);

    worker.postMessage({
      scripts: phantasus.Util.getScriptPath(),
      dataset: phantasus.Dataset.toJSON(dataset, {
        columnFields: [],
        rowFields: [],
        seriesIndices: [0]
      }),
      input: options.input
    });

    worker.onmessage = function (e) {
      var name = heatMap.getName();
      var matrix = e.data;
      var n = isColumnMatrix ? dataset.getColumnCount() : dataset
        .getRowCount();
      var d = new phantasus.Dataset({
        name: name,
        rows: n,
        columns: n
      });
      // set the diagonal
      var isDistance = f.toString() === phantasus.Euclidean.toString()
        || f.toString() === phantasus.Jaccard.toString();
      for (var i = 1; i < n; i++) {
        for (var j = 0; j < i; j++) {
          var value = matrix[i][j];
          d.setValue(i, j, value);
          d.setValue(j, i, value);
        }
      }
      // no need to set diagonal if not distance as array already
      // initialized to 0
      if (!isDistance) {
        for (var i = 0; i < n; i++) {
          d.setValue(i, i, 1);
        }
      }
      var metadata = isColumnMatrix ? dataset.getColumnMetadata()
        : dataset.getRowMetadata();
      d.rowMetadataModel = phantasus.MetadataUtil.shallowCopy(metadata);
      d.columnMetadataModel = phantasus.MetadataUtil.shallowCopy(metadata);
      var colorScheme;
      if (!isDistance) {
        colorScheme = {
          type: 'fixed',
          map: [{
            value: -1,
            color: 'blue'
          }, {
            value: 0,
            color: 'white'
          }, {
            value: 1,
            color: 'red'
          }]
        };
      } else {
        colorScheme = {
          type: 'fixed',
          map: [{
            value: 0,
            color: 'white'
          }, {
            value: phantasus.DatasetUtil.max(d),
            color: 'red'
          }]
        };
      }
      new phantasus.HeatMap({
        colorScheme: colorScheme,
        name: name,
        dataset: d,
        parent: heatMap,
        inheritFromParentOptions: {
          rows: !isColumnMatrix,
          columns: isColumnMatrix
        }
      });
      worker.terminate();
      window.URL.revokeObjectURL(url);
    };
    return worker;
  }
};

phantasus.TransposeTool = function () {
};
phantasus.TransposeTool.prototype = {
  toString: function () {
    return 'Transpose';
  },
  execute: function (options) {
    var project = options.project;
    var heatMap = options.heatMap;
    var dataset = new phantasus.TransposedDatasetView(project
      .getSortedFilteredDataset());
    // make a shallow copy of the dataset, metadata is immutable via the UI
    var rowMetadataModel = phantasus.MetadataUtil.shallowCopy(dataset
      .getRowMetadata());
    var columnMetadataModel = phantasus.MetadataUtil.shallowCopy(dataset
      .getColumnMetadata());
    dataset.getRowMetadata = function () {
      return rowMetadataModel;
    };
    dataset.getColumnMetadata = function () {
      return columnMetadataModel;
    };

    // TODO see if we can subset dendrograms
    // only handle contiguous selections for now
    // if (heatMap.columnDendrogram != null) {
    // var indices = project.getColumnSelectionModel().getViewIndices()
    // .toArray();
    // phantasus.DendrogramUtil.leastCommonAncestor();
    // }
    // if (heatMap.rowDendrogram != null) {
    //
    // }
    var name = options.input.name || heatMap.getName();
    new phantasus.HeatMap({
      name: name,
      dataset: dataset,
      inheritFromParentOptions: {
        transpose: true
      },
      parent: heatMap
    });

  }
};

phantasus.TsneTool = function () {
};

phantasus.TsneTool.execute = function (dataset, input) {
  // note: in worker here
  var matrix = [];
  var rows = input.project == 'Rows';
  if (!rows) {
    dataset = new phantasus.TransposedDatasetView(dataset);
  }
  var N = dataset.getRowCount();
  var f = phantasus.HClusterTool.Functions.fromString(input.metric);
  if (f === phantasus.TsneTool.PRECOMPUTED_DIST) {
    for (var i = 0; i < N; i++) {
      matrix.push([]);
      for (var j = i + 1; j < N; j++) {
        matrix[i][j] = dataset.getValue(i, j);
      }
    }
  } else if (f === phantasus.TsneTool.PRECOMPUTED_SIM) {
    var max = phantasus.DatasetUtil.max(dataset);
    for (var i = 0; i < N; i++) {
      matrix.push([]);
      for (var j = i + 1; j < N; j++) {
        matrix[i][j] = max - dataset.getValue(i, j);
      }
    }
  } else {
    var list1 = new phantasus.DatasetRowView(dataset);
    var list2 = new phantasus.DatasetRowView(dataset);
    for (var i = 0; i < N; i++) {
      matrix.push([]);
      list1.setIndex(i);
      for (var j = i + 1; j < N; j++) {
        var d = f(list1, list2.setIndex(j));
        matrix[i][j] = d;
      }
    }
  }
  var opt = {};
  opt.epsilon = input.epsilon;
  opt.perplexity = input.perplexity;
  opt.dim = 2;
  var tsne = new tsnejs.tSNE(opt);
  tsne.initDataDist(matrix);
  for (var k = 0; k < 1000; k++) {
    tsne.step();
  }
  var Y = tsne.getSolution();
  return {solution: Y};

}
;
phantasus.TsneTool.prototype = {
  toString: function () {
    return 't-SNE';
  },
  init: function (project, form) {

  },
  gui: function () {
    return [{
      name: 'metric',
      options: phantasus.HClusterTool.Functions,
      value: phantasus.HClusterTool.Functions[3].toString(),
      type: 'select'
    }, {
      name: 'project',
      options: ['Columns', 'Rows'],
      value: 'Columns',
      type: 'select'
    }, {
      name: 'epsilon',
      value: '10',
      type: 'text',
      help: 'learning rate'
    }, {
      name: 'perplexity',
      value: '30',
      type: 'text',
      help: 'number of effective nearest neighbors'
    }];
  },
  execute: function (options) {
    var project = options.project;
    var heatMap = options.heatMap;
    var rows = options.input.project == 'Rows';
    var dataset = project.getSortedFilteredDataset();
    options.input.epsilon = parseInt(options.input.epsilon);
    options.input.perplexity = parseInt(options.input.perplexity);
    var blob = new Blob(
      ['self.onmessage = function(e) {'
      + 'e.data.scripts.forEach(function (s) { importScripts(s); });'
      + 'self.postMessage(phantasus.TsneTool.execute(phantasus.Dataset.fromJSON(e.data.dataset), e.data.input));'
      + '}']);

    var url = URL.createObjectURL(blob);
    var worker = new Worker(url);

    worker.postMessage({
      scripts: [phantasus.Util.getScriptPath()],
      dataset: phantasus.Dataset.toJSON(dataset, {
        columnFields: [],
        rowFields: [],
        seriesIndices: [0]
      }),
      input: options.input
    });

    worker.onmessage = function (e) {
      if (rows) {
        dataset = new phantasus.TransposedDatasetView(dataset);
      }
      var result = e.data.solution;

      var newDataset = new phantasus.Dataset({
        name: 't-SNE',
        rows: dataset.getColumnCount(),
        columns: 2
      });

      for (var i = 0; i < result.length; i++) {
        newDataset.setValue(i, 0, result[i][0]);
        newDataset.setValue(i, 1, result[i][1]);
      }
      var idVector = newDataset.getColumnMetadata().add('id');
      idVector.setValue(0, 'P1');
      idVector.setValue(1, 'P2');
      newDataset.setRowMetadata(phantasus.MetadataUtil.shallowCopy(dataset.getColumnMetadata()));
      var min = phantasus.DatasetUtil.min(newDataset);
      var max = phantasus.DatasetUtil.max(newDataset);
      new phantasus.HeatMap({
        inheritFromParentOptions: {transpose: !rows},
        name: 't-SNE',
        dataset: newDataset,
        parent: heatMap,
        columns: [{
          field: 'id',
          display: 'text'
        }],
        colorScheme: {
          type: 'fixed',
          map: [{
            value: min,
            color: colorbrewer.Greens[3][0]
          }, {
            value: max,
            color: colorbrewer.Greens[3][2]
          }]
        }
      });
      worker.terminate();
      window.URL.revokeObjectURL(url);
    };
    return worker;
  }
};

phantasus.AbstractCanvas = function (offscreen) {
  this.canvas = phantasus.CanvasUtil.createCanvas();
  this.lastClip = null;
  if (offscreen) {
    this.offscreenCanvas = phantasus.CanvasUtil.createCanvas();
  }
  this.offset = {
    x: 0,
    y: 0
  };
};

phantasus.AbstractCanvas.prototype = {
  visible: true,
  invalid: true,
  scrollX: 0,
  scrollY: 0,
  prefWidth: undefined,
  prefHeight: undefined,
  getCanvas: function () {
    return this.canvas;
  },
  scrollTop: function (pos) {
    if (pos === undefined) {
      return this.offset.y;
    }
    this.offset.y = pos;
  },
  appendTo: function ($el) {
    // if (this.offscreenCanvas) {
    // $(this.offscreenCanvas).appendTo($el);
    // }
    $(this.canvas).appendTo($el);
  },
  scrollLeft: function (pos) {
    if (pos === undefined) {
      return this.offset.x;
    }
    this.offset.x = pos;
  },
  dispose: function () {
    $(this.canvas).remove();
    this.offscreenCanvas = undefined;
  },
  getPrefWidth: function () {
    return this.prefWidth;
  },
  /**
   * Tells this canvas to invalidate any offscreen cached images
   */
  setInvalid: function (invalid) {
    this.invalid = invalid;
  },
  setBounds: function (bounds) {
    var backingScale = phantasus.CanvasUtil.BACKING_SCALE;
    var canvases = [this.canvas];
    if (this.offscreenCanvas) {
      canvases.push(this.offscreenCanvas);
    }
    if (bounds.height != null) {
      _.each(canvases, function (canvas) {
        canvas.height = bounds.height * backingScale;
        canvas.style.height = bounds.height + 'px';
      });
    }
    if (bounds.width != null) {
      _.each(canvases, function (canvas) {
        canvas.width = bounds.width * backingScale;
        canvas.style.width = bounds.width + 'px';
      });
    }
    if (bounds.left != null) {
      _.each(canvases, function (canvas) {
        canvas.style.left = bounds.left + 'px';
      });
    }
    if (bounds.top != null) {
      _.each(canvases, function (canvas) {
        canvas.style.top = bounds.top + 'px';
      });
    }
  },
  /**
   * Paint this canvas using the specified clip.
   */
  paint: function (clip) {
    var canvas = this.canvas;
    var context = canvas.getContext('2d');
    phantasus.CanvasUtil.resetTransform(context);
    var width = this.getUnscaledWidth();
    var height = this.getUnscaledHeight();
    context.clearRect(0, 0, width, height);
    if (this.prePaint) {
      phantasus.CanvasUtil.resetTransform(context);
      context.translate(this.offset.x, this.offset.y);
      this.prePaint(clip, context);
    }
    phantasus.CanvasUtil.resetTransform(context);
    if (this.offscreenCanvas) {
      if (this.invalid) {
        var oc = this.offscreenCanvas.getContext('2d');
        phantasus.CanvasUtil.resetTransform(oc);
        context.translate(this.offset.x, this.offset.y);
        oc.clearRect(0, 0, width, height);
        this.draw(clip, oc);
      }
      context.drawImage(this.offscreenCanvas, 0, 0, width, height);
    } else {
      this.draw(clip, context);
    }
    if (this.postPaint) {
      phantasus.CanvasUtil.resetTransform(context);
      context.translate(this.offset.x, this.offset.y);
      this.postPaint(clip, context);
    }
    this.lastClip = clip;
    this.invalid = false;
  },
  repaint: function () {
    if (!this.lastClip) {
      this.lastClip = {
        x: 0,
        y: 0,
        width: this.getUnscaledWidth(),
        height: this.getUnscaledHeight()
      };
    }
    this.paint(this.lastClip);
  },
  /**
   * Draw this canvas into the specified context.
   */
  draw: function (clip, context) {
    console.log('Not implemented');
  },
  getPrefHeight: function () {
    return this.prefHeight;
  },
  setPrefWidth: function (prefWidth) {
    this.prefWidth = prefWidth;
  },
  setPrefHeight: function (prefHeight) {
    this.prefHeight = prefHeight;
  },
  isVisible: function () {
    return this.visible;
  },
  setVisible: function (visible) {
    if (this.visible !== visible) {
      this.visible = visible;
      this.canvas.style.display = visible ? '' : 'none';
    }
  },
  getUnscaledWidth: function () {
    return this.canvas.width / phantasus.CanvasUtil.BACKING_SCALE;
  },
  getUnscaledHeight: function () {
    return this.canvas.height / phantasus.CanvasUtil.BACKING_SCALE;
  },
  getWidth: function () {
    return this.canvas.width;
  },
  getHeight: function () {
    return this.canvas.height;
  }
};

phantasus.AbstractColorSupplier = function () {
  this.fractions = [0, 0.5, 1];
  this.colors = ['#0000ff', '#ffffff', '#ff0000'];
  this.names = null; // optional color stop names
  this.min = 0;
  this.max = 1;
  this.missingColor = '#c0c0c0';
  this.scalingMode = phantasus.HeatMapColorScheme.ScalingMode.RELATIVE;
  this.stepped = false;
  this.sizer = new phantasus.HeatMapSizer();
  this.conditions = new phantasus.HeatMapConditions();
  this.transformValues = 0;// z-score, robust z-score
};
phantasus.AbstractColorSupplier.Z_SCORE = 1;
phantasus.AbstractColorSupplier.ROBUST_Z_SCORE = 2;

phantasus.AbstractColorSupplier.toJSON = function (cs) {
  var json = {
    fractions: cs.fractions,
    colors: cs.colors,
    min: cs.min,
    max: cs.max,
    missingColor: cs.missingColor,
    scalingMode: cs.scalingMode,
    stepped: cs.stepped,
    transformValues: cs.transformValues
  };
  if (cs.names) {
    json.names = cs.names;
  }
  if (cs.conditions && cs.conditions.array.length > 0) {
    json.conditions = cs.conditions.array;
  }
  if (cs.sizer && cs.sizer.seriesName != null) {
    json.size = {
      seriesName: cs.sizer.seriesName,
      min: cs.sizer.min,
      max: cs.sizer.max
    };
  }
  return json;
};
phantasus.AbstractColorSupplier.fromJSON = function (json) {
  var cs = json.stepped ? new phantasus.SteppedColorSupplier()
    : new phantasus.GradientColorSupplier();

  if (json.scalingMode == null && json.type != null) {
    json.scalingMode = json.type; // old
  }
  if (json.scalingMode === 'relative' || json.scalingMode === 0) {
    json.scalingMode = 0;
  } else if (json.scalingMode === 'fixed' || json.scalingMode === 1) {
    json.scalingMode = 1;
  } else { // default to relative
    json.scalingMode = 0;
  }
  cs.setScalingMode(json.scalingMode);
  if (json.min != null) {
    cs.setMin(json.min);
  }
  if (json.max != null) {
    cs.setMax(json.max);
  }
  if (json.missingColor != null) {
    cs.setMissingColor(json.missingColor);
  }
  if (phantasus.HeatMapColorScheme.ScalingMode.RELATIVE !== json.scalingMode) {
    cs.setTransformValues(json.transformValues);
  }

  if (json.map) { // old
    json.values = json.map.map(function (item) {
      return item.value;
    });
    json.colors = json.map.map(function (item) {
      return item.color;
    });
  }
  var fractions = json.fractions;
  if (json.values) { // map values to fractions
    fractions = [];
    var values = json.values;
    var min = Number.MAX_VALUE;
    var max = -Number.MAX_VALUE;
    for (var i = 0; i < values.length; i++) {
      var value = values[i];
      min = Math.min(min, value);
      max = Math.max(max, value);
    }
    var valueToFraction = d3.scale.linear().domain(
      [min, max]).range(
      [0, 1]).clamp(true);

    for (var i = 0; i < values.length; i++) {
      fractions.push(valueToFraction(values[i]));
    }
    if (json.min == null) {
      cs.setMin(min);
    }
    if (json.max == null) {
      cs.setMax(max);
    }
  }
  if (json.colors != null && json.colors.length > 0) {
    cs.setFractions({
      colors: json.colors,
      fractions: fractions,
      names: json.names
    });
  }
  if (json.size) {
    cs.getSizer().setSeriesName(json.size.seriesName);
    cs.getSizer().setMin(json.size.min);
    cs.getSizer().setMax(json.size.max);
  }

  if (json.conditions && _.isArray(json.conditions)) {
    // load conditions
    json.conditions.forEach(function (condition) {
      var gtf = function () {
        return true;
      };
      var ltf = function () {
        return true;
      };
      if (condition.seriesName == null) {
        condition.seriesName = condition.series; // series is deprecated
      }
      if (condition.v1 != null && !isNaN(condition.v1)) {
        gtf = condition.v1Op === 'gt' ? function (val) {
          return val > condition.v1;
        } : function (val) {
          return val >= condition.v1;
        };
      }

      if (condition.v2 != null && !isNaN(condition.v2)) {
        ltf = condition.v2Op === 'lt' ? function (val) {
          return val < condition.v2;
        } : function (val) {
          return val <= condition.v2;
        };
      }
      condition.accept = function (val) {
        return gtf(val) && ltf(val);
      };
    });
    cs.conditions.array = json.conditions;
  }
  return cs;
};

phantasus.AbstractColorSupplier.prototype = {
  getTransformValues: function () {
    return this.transformValues;
  },
  setTransformValues: function (transformValues) {
    this.transformValues = transformValues;
  },
  getSizer: function () {
    return this.sizer;
  },
  getConditions: function () {
    return this.conditions;
  },
  createInstance: function () {
    throw 'not implemented';
  },
  copy: function () {
    var c = this.createInstance();
    c.stepped = this.stepped;
    c.setFractions({
      fractions: this.fractions.slice(0),
      colors: this.colors.slice(0)
    });
    if (this.names != null) {
      c.names = this.names.slice(0);
    }
    if (this.sizer) {
      c.sizer = this.sizer.copy();
    }
    if (this.conditions) {
      c.conditions = this.conditions.copy();
    }
    c.scalingMode = this.scalingMode;
    c.min = this.min;
    c.max = this.max;
    c.missingColor = this.missingColor;
    if (this.scalingMode !== phantasus.HeatMapColorScheme.ScalingMode.RELATIVE) {
      c.transformValues = this.transformValues;
    }

    return c;
  },
  setMissingColor: function (missingColor) {
    this.missingColor = missingColor;
  },
  getMissingColor: function () {
    return this.missingColor;
  },
  getScalingMode: function () {
    return this.scalingMode;
  },
  setScalingMode: function (scalingMode) {
    if (scalingMode !== this.scalingMode) {
      if (scalingMode === phantasus.HeatMapColorScheme.ScalingMode.RELATIVE) {
        this.min = 0;
        this.max = 1;
      }
      this.scalingMode = scalingMode;
    }
  },
  isStepped: function () {
    return false;
  },
  getColor: function (row, column, value) {
    throw 'not implemented';
  },
  getColors: function () {
    return this.colors;
  },
  getNames: function () {
    return this.names;
  },
  getFractions: function () {
    return this.fractions;
  },
  getMin: function () {
    return this.min;
  },
  getMax: function () {
    return this.max;
  },
  setMin: function (min) {
    this.min = min;
  },
  setMax: function (max) {
    // the min and max are set by heat map color scheme for each row
    this.max = max;
  },
  /**
   *
   * @param options.fractions
   *            Array of stop fractions
   * @param options.colors
   *            Array of stop colors
   * @param options.names
   *            Array of stop names
   */
  setFractions: function (options) {
    var index = phantasus.Util.indexSort(options.fractions, true);
    this.fractions = phantasus.Util.reorderArray(options.fractions, index);
    this.colors = phantasus.Util.reorderArray(options.colors, index);
    this.names = options.names ? phantasus.Util.reorderArray(options.names,
      index) : null;
  }
};

phantasus.AbstractComponent = function () {
  this.lastClip = null;
  var c = document.createElement('div');
  c.setAttribute('tabindex', '0');
  c.style.outline = 0;
  c.style.overflow = 'hidden';
  c.style.position = 'absolute';
  this.el = c;
  this.$el = $(c);
};
phantasus.AbstractComponent.prototype = {
  visible: true,
  invalid: true,
  prefWidth: undefined,
  prefHeight: undefined,
  appendTo: function ($el) {
    $(this.el).appendTo($el);
  },
  dispose: function () {
    $(this.el).remove();
  },
  getPrefWidth: function () {
    return this.prefWidth;
  },
  /**
   * Tells this component to invalidate
   */
  setInvalid: function (invalid) {
    this.invalid = invalid;
  },
  setBounds: function (bounds) {
//		if (bounds.height != null) {
//			this.el.style.height = bounds.height + 'px';
//		}
//		if (bounds.width != null) {
//			this.el.style.width = bounds.width + 'px';
//		}
    if (bounds.left != null) {
      this.$el.css('left', bounds.left + 'px');
    }
    if (bounds.top != null) {
      this.$el.css('top', bounds.top + 'px');
    }
  },
  /**
   * Paint this canvas using the specified clip.
   */
  paint: function (clip) {
    var width = this.getUnscaledWidth();
    var height = this.getUnscaledHeight();
    this.draw(clip);
    this.lastClip = clip;
    this.invalid = false;
  },
  repaint: function () {
    if (!this.lastClip) {
      this.lastClip = {
        x: 0,
        y: 0,
        width: this.getUnscaledWidth(),
        height: this.getUnscaledHeight()
      };
    }
    this.paint(this.lastClip);
  },
  /**
   * Draw this canvas into the specified context.
   */
  draw: function (clip) {
  },
  getPrefHeight: function () {
    return this.prefHeight;
  },
  setPrefWidth: function (prefWidth) {
    this.prefWidth = prefWidth;
  },
  setPrefHeight: function (prefHeight) {
    this.prefHeight = prefHeight;
  },
  isVisible: function () {
    return this.visible;
  },
  setVisible: function (visible) {
    if (this.visible !== visible) {
      this.visible = visible;
      this.el.style.display = visible ? '' : 'none';
    }
  },
  getUnscaledWidth: function () {
    return this.$el.width();
  },
  getUnscaledHeight: function () {
    return this.$el.height();
  },
  getWidth: function () {
    return this.$el.width();
  },
  getHeight: function () {
    return this.$el.height();
  }
};

/*
 *
 * @param tree An object with maxHeight, rootNode, leafNodes, nLeafNodes. Each node has an id
 * (integer), name (string), children, depth, height, minIndex, maxIndex, parent. Leaf nodes also
 * have an index.
 The root has the largest height, leaves the smallest height.

 */
phantasus.AbstractDendrogram = function (heatMap, tree, positions, project,
                                        type) {
  phantasus.AbstractCanvas.call(this, true);

  this._overviewHighlightColor = '#d8b365';
  this._searchHighlightColor = '#e41a1c';
  this._selectedNodeColor = type === phantasus.AbstractDendrogram.Type.COLUMN ? '#377eb8'
    : '#984ea3';
  this.tree = tree;
  this.type = type;
  this.squishEnabled = false;
  this.heatMap = heatMap;
  this.positions = positions;
  this.project = project;
  var $label = $('<span></span>');
  $label.addClass('label label-info');
  $label.css('position', 'absolute');
  this.$label = $label;
  var $squishedLabel = $('<span></span>');
  $squishedLabel.addClass('label label-default');
  $squishedLabel.css('position', 'absolute').css('top', 18);
  this.$squishedLabel = $squishedLabel;
  this.$label = $label;
  this.cutHeight = this.tree.maxHeight;
  this.drawLeafNodes = true;
  this.lineWidth = 0.7;
  this.selectedNodeIds = {};
  this.selectedRootNodeIdToNode = {};
  this.nodeIdToHighlightedPathsToRoot = {};
  var _this = this;
  this.defaultStroke = 'rgb(0,0,0)';
  this.mouseMoveNodes = null;
  var mouseMove = function (event) {
    if (!phantasus.CanvasUtil.dragging) {
      var position = phantasus.CanvasUtil.getMousePosWithScroll(
        event.target, event, _this.lastClip.x, _this.lastClip.y);
      if (_this.isDragHotSpot(position)) { // dendrogram cutter
        _this.canvas.style.cursor = _this.getResizeCursor();
      } else {
        var nodes;
        if (_this.getNodes) {
          nodes = _this.getNodes(position);
        } else {
          var node = _this.getNode(position);
          if (node) {
            nodes = [node];
          }
        }
        _this.mouseMoveNodes = nodes;
        if (nodes != null) {
          nodes.sort(function (a, b) {
            return a.name < b.name;
          });
          var tipOptions = {
            event: event
          };
          tipOptions[type === phantasus.AbstractDendrogram.Type.COLUMN ? 'columnNodes'
            : 'rowNodes'] = nodes;
          _this.heatMap.setToolTip(-1, -1, tipOptions);
          _this.canvas.style.cursor = 'pointer';
        } else {
          _this.heatMap.setToolTip(-1, -1);
          _this.canvas.style.cursor = 'default';
        }
      }
    }
  };
  var mouseExit = function (e) {
    if (!phantasus.CanvasUtil.dragging) {
      _this.mouseMoveNodes = null;
      _this.canvas.style.cursor = 'default';
    }
  };
  if (type !== phantasus.AbstractDendrogram.Type.RADIAL) {

    $(this.canvas)
      .on(
        'contextmenu',
        function (e) {
          e.preventDefault();
          e.stopPropagation();
          e.stopImmediatePropagation();
          var position = phantasus.CanvasUtil.getMousePosWithScroll(e.target,
              e, _this.lastClip.x,
              _this.lastClip.y);
          var selectedNode = _this.getNode(position);
          phantasus.Popup.showPopup(
              [
                {
                name: 'Flip',
                disabled: selectedNode == null
              }, {
                name: 'Branch Color',
                disabled: selectedNode == null
              }, {
                separator: true
              },
                {
                  name: 'Annotate...'
                }, {
                name: 'Save'
              }, {
                separator: true
              }, {
                name: 'Enrichment...'
              }, {
                separator: true
              }, {
                name: 'Squish Singleton Clusters',
                checked: _this.squishEnabled
              }, {
                separator: true
              }, {
                name: 'Delete'
              }],
              {
                x: e.pageX,
                y: e.pageY
              },
              e.target,
              function (menuItem, item) {
                if (item === 'Save') {
                  var formBuilder = new phantasus.FormBuilder();
                  formBuilder.append({
                    name: 'file_name',
                    type: 'text',
                    required: true,
                  });
                  formBuilder.append({
                    name: 'leaf_node_id_field',
                    type: 'bootstrap-select',
                    required: true,
                    options: phantasus.MetadataUtil.getMetadataNames(
                      type === phantasus.AbstractDendrogram.Type.COLUMN
                        ? project.getFullDataset().getColumnMetadata()
                        : project.getFullDataset().getRowMetadata())
                  });
                  phantasus.FormBuilder.showOkCancel({
                    title: 'Save Dendrogram',
                    content: formBuilder.$form,
                    focus: document.activeElement,
                    okCallback: function () {
                      var fileName = formBuilder.getValue('file_name');
                      if (fileName === '') {
                        fileName = 'dendrogram.txt';
                      }
                      var leafNodeIdField = formBuilder.getValue('leaf_node_id_field');
                      var out = [];
                      var vector = type === phantasus.AbstractDendrogram.Type.COLUMN
                        ? project.getFullDataset().getColumnMetadata().getByName(leafNodeIdField)
                        : project.getFullDataset().getRowMetadata().getByName(leafNodeIdField);
                      var leafNodeToString = function (n) {
                        return vector.getValue(n.index);
                      };
                      phantasus.DendrogramUtil.writeNewick(tree.rootNode, out, leafNodeToString);
                      var blob = new Blob([out.join('')], {type: 'text/plain;charset=charset=utf-8'});
                      saveAs(blob, fileName, true);
                    }
                  });
                } else if (item === 'Flip') {
                  if (selectedNode != null) {
                    var isColumns = phantasus.AbstractDendrogram.Type.COLUMN === _this.type;
                    var min = selectedNode.minIndex;
                    var max = selectedNode.maxIndex;

                    // phantasus.DendrogramUtil.dfs(selectedNode, function (n) {
                    //   if (n.children) {
                    //     n.children.reverse();
                    //   }
                    //   return true;
                    // });

                    var leafNodes = tree.leafNodes;
                    for (var i = min, index = max; i <= max; i++, index--) {
                      var n = leafNodes[i];
                      n.index = index;
                      n.maxIndex = index;
                      n.minIndex = index;
                    }

                    leafNodes.sort(function (a, b) {
                      return (a.index < b.index ? -1 : 1);
                    });
                    var setIndex = function (n) {
                      if (n.children != null && n.children.length > 0) {
                        for (var i = 0; i < n.children.length; i++) {
                          setIndex(n.children[i]);
                        }
                        var sum = 0;
                        for (var i = 0; i < n.children.length; i++) {
                          sum += n.children[i].index;
                        }
                        n.index = sum / n.children.length;
                        var maxIndex = -Number.MAX_VALUE;
                        var minIndex = Number.MAX_VALUE;
                        for (var i = 0; i < n.children.length; i++) {
                          maxIndex = Math.max(maxIndex, n.children[i].maxIndex);
                          minIndex = Math.min(minIndex, n.children[i].minIndex);
                        }
                        n.minIndex = minIndex;
                        n.maxIndex = maxIndex;
                      }
                    };

                    setIndex(selectedNode);

                var currentOrder = [];
                var count = isColumns ? heatMap.getProject().getSortedFilteredDataset().getColumnCount() : heatMap.getProject()
                  .getSortedFilteredDataset()
                  .getRowCount();
                for (var i = 0; i < count; i++) {
                  currentOrder.push(isColumns ? project.convertViewColumnIndexToModel(i) : project.convertViewRowIndexToModel(i));
                }
                for (var i = min, j = max; i < j; i++, j--) {
                  var tmp = currentOrder[j];
                  currentOrder[j] = currentOrder[i];
                  currentOrder[i] = tmp;
                }
                var key = new phantasus.SpecifiedModelSortOrder(currentOrder, currentOrder.length, 'dendrogram', isColumns);
                key.setPreservesDendrogram(true);
                key.setLockOrder(2);
                key.setUnlockable(false);
                if (isColumns) {
                  heatMap.getProject().setColumnSortKeys([key], true);
                } else {
                  heatMap.getProject().setRowSortKeys([key], true);
                }
                heatMap.revalidate();
              }

                } else if (item === 'Branch Color') {
                  if (selectedNode != null) {
                    var formBuilder = new phantasus.FormBuilder();
                    formBuilder.append({
                      name: 'color',
                      type: 'color',
                      value: selectedNode.color,
                      required: true,
                      style: 'max-width:50px;'
                    });
                    formBuilder.find('color').on(
                      'change',
                      function () {
                        var color = $(this).val();
                        phantasus.DendrogramUtil.dfs(selectedNode, function (n) {
                          n.color = color;
                          return true;
                        });
                        _this.setSelectedNode(null);
                      });
                    phantasus.FormBuilder.showInModal({
                      title: 'Color',
                      close: 'Close',
                      html: formBuilder.$form,
                      focus: document.activeElement
                    });

                  }
                } else if (item === 'Annotate...') {
                  phantasus.HeatMap.showTool(
                      new phantasus.AnnotateDendrogramTool(
                        type === phantasus.AbstractDendrogram.Type.COLUMN),
                      _this.heatMap);
                } else if (item === 'Enrichment...') {
                  phantasus.HeatMap.showTool(
                      new phantasus.DendrogramEnrichmentTool(
                        type === phantasus.AbstractDendrogram.Type.COLUMN),
                      _this.heatMap);
                } else if (item === 'Squish Singleton Clusters') {
                  _this.squishEnabled = !_this.squishEnabled;
                  if (!_this.squishEnabled) {
                    _this.positions.setSquishedIndices(null);
                  }
                } else if (item === 'Delete') {
                  _this.resetCutHeight();
                  _this.heatMap.setDendrogram(
                      null,
                      type === phantasus.AbstractDendrogram.Type.COLUMN);
                }
              });
          return false;
        });

    $(this.canvas).on('mousemove', _.throttle(mouseMove, 100)).on(
      'mouseout', _.throttle(mouseExit, 100)).on('mouseenter',
      _.throttle(mouseMove, 100));
  }
  var dragStartScaledCutHeight = 0;
  this.cutTreeHotSpot = false;
  if (type !== phantasus.AbstractDendrogram.Type.RADIAL) {
    this.hammer = phantasus.Util.hammer(this.canvas, ['pan', 'tap']).on(
        'tap',
        this.tap = function (event) {
          if (!phantasus.CanvasUtil.dragging) {
            var position = phantasus.CanvasUtil.getMousePosWithScroll(event.target,
                event, _this.lastClip.x,
                _this.lastClip.y);
            _this.cutTreeHotSpot = _this.isDragHotSpot(position);
            if (_this.cutTreeHotSpot) {
              return;
            }
            var node = _this.getNode(position);
            if (node != null && node.parent === undefined) {
              node = null; // can't select root
            }
            var commandKey = phantasus.Util.IS_MAC ? event.srcEvent.metaKey
              : event.srcEvent.ctrlKey;
            _this.setSelectedNode(node,
              event.srcEvent.shiftKey || commandKey);
          }
        }).on('panend', this.panend = function (event) {
        phantasus.CanvasUtil.dragging = false;
        _this.canvas.style.cursor = 'default';
        _this.cutTreeHotSpot = true;
      }).on(
        'panstart',
        this.panstart = function (event) {
          var position = phantasus.CanvasUtil.getMousePosWithScroll(event.target, event,
              _this.lastClip.x, _this.lastClip.y,
              true);
          _this.cutTreeHotSpot = _this.isDragHotSpot(position);
          if (_this.cutTreeHotSpot) { // make sure start event
            // was on hotspot
            phantasus.CanvasUtil.dragging = true;
            _this.canvas.style.cursor = _this.getResizeCursor();
            dragStartScaledCutHeight = _this.scale(_this.cutHeight);
          }
        }).on(
        'panmove',
        this.panmove = function (event) {
          if (_this.cutTreeHotSpot) {
            var cutHeight;
            if (_this.type === phantasus.AbstractDendrogram.Type.COLUMN) {
              var delta = event.deltaY;
              cutHeight = Math.max(
                  0,
                  Math.min(
                      _this.tree.maxHeight,
                      _this.scale.invert(dragStartScaledCutHeight
                          + delta)));
            } else if (_this.type === phantasus.AbstractDendrogram.Type.ROW) {
              var delta = event.deltaX;
              cutHeight = Math.max(
                  0,
                  Math.min(
                      _this.tree.maxHeight,
                      _this.scale.invert(dragStartScaledCutHeight
                          + delta)));
            } else {
              var point = phantasus.CanvasUtil.getMousePos(event.target, event);
              point.x = _this.radius - point.x;
              point.y = _this.radius - point.y;
              var radius = Math.sqrt(point.x * point.x
                + point.y * point.y);
              if (radius <= 4) {
                cutHeight = _this.tree.maxHeight;
              } else {
                cutHeight = Math.max(0, Math.min(
                  _this.tree.maxHeight,
                  _this.scale.invert(radius)));
              }
            }
            if (cutHeight >= _this.tree.maxHeight) {
              _this.resetCutHeight();
            } else {
              _this.setCutHeight(cutHeight);
            }
            event.preventDefault();
          }
        });
  }
};
phantasus.AbstractDendrogram.Type = {
  COLUMN: 0,
  ROW: 1,
  RADIAL: 2
};
phantasus.AbstractDendrogram.prototype = {
  setSelectedNode: function (node, add) {
    var _this = this;
    var viewIndices;
    var selectionModel = this.type === phantasus.AbstractDendrogram.Type.COLUMN ? this.project.getColumnSelectionModel()
      : this.project.getRowSelectionModel();
    if (node == null) {
      // clear selection
      _this.selectedNodeIds = {};
      _this.selectedRootNodeIdToNode = {};
      viewIndices = new phantasus.Set();
    } else {
      if (add) { // add to selection
        viewIndices = selectionModel.getViewIndices();
      } else {
        viewIndices = new phantasus.Set();
        _this.selectedNodeIds = {};
        _this.selectedRootNodeIdToNode = {};
      }
      if (node != null) {
        if (node.children === undefined) { // leaf node
          var contains = _this.nodeIdToHighlightedPathsToRoot[node.id];
          if (!add) {
            _this.nodeIdToHighlightedPathsToRoot = {};
          }
          if (contains) {
            delete _this.nodeIdToHighlightedPathsToRoot[node.id];
            // toggle
          } else {
            _this.nodeIdToHighlightedPathsToRoot[node.id] = node;
          }
        } else {
          _this.selectedRootNodeIdToNode[node.id] = node;
          phantasus.DendrogramUtil.dfs(node, function (d) {
            _this.selectedNodeIds[d.id] = true;
            return true;
          });
        }
        for (var i = node.minIndex; i <= node.maxIndex; i++) {
          viewIndices.add(i);
        }
      }
    }
    _this.trigger('nodeSelectionChanged', _this.selectedRootNodeIdToNode);
    selectionModel.setViewIndices(viewIndices, true);
    _this.repaint();
  },
  getPathStroke: function (node) {
    if (this.selectedNodeIds[node.id]) {
      return this._selectedNodeColor;
    }
    if (node.color !== undefined) {
      return node.color;
    }
    // if (node.search) {
    // return this._searchHighlightColor;
    // }
    return this.defaultStroke;
  },
  /**
   *
   * @param node
   * @return The color, if any, to draw a circle for a node in the dendrogram
   */
  getNodeFill: function (node) {
    if (this.selectedRootNodeIdToNode[node.id]) {
      return this._selectedNodeColor;
    }
    if (node.search) {
      return this._searchHighlightColor;
    }
    if (node.info !== undefined) {
      return this._overviewHighlightColor;
    }

  },
  resetCutHeight: function () {
    this.positions.setSquishedIndices(null);
    if (this.type === phantasus.AbstractDendrogram.Type.COLUMN) {
      this.project.setGroupColumns([], true);
    } else {
      this.project.setGroupRows([], true);
    }
    this.$label.text('');
    this.$squishedLabel.text('');
    var dataset = this.project.getSortedFilteredDataset();
    var clusterIdVector = this.type === phantasus.AbstractDendrogram.Type.COLUMN ? dataset
      .getColumnMetadata().getByName('dendrogram_cut')
      : dataset.getRowMetadata().getByName('dendrogram_cut');
    if (clusterIdVector) {
      for (var i = 0, size = clusterIdVector.size(); i < size; i++) {
        clusterIdVector.setValue(i, NaN);
      }
    }
  },
  setCutHeight: function (height) {
    this.cutHeight = height;
    var squishedIndices = {};
    var clusterNumber = 0;
    var nsquished = 0;

    var squishEnabled = this.squishEnabled;
    var roots = phantasus.DendrogramUtil.cutAtHeight(this.tree.rootNode,
      this.cutHeight);
    var dataset = this.project.getSortedFilteredDataset();
    var clusterIdVector = this.type === phantasus.AbstractDendrogram.Type.COLUMN ? dataset.getColumnMetadata().add('dendrogram_cut')
      : dataset.getRowMetadata().add('dendrogram_cut');
    for (var i = 0, nroots = roots.length; i < nroots; i++) {
      var root = roots[i];
      var minChild = phantasus.DendrogramUtil.getDeepestChild(root,
        true);
      var maxChild = phantasus.DendrogramUtil.getDeepestChild(root,
        false);
      var clusterId;
      if (squishEnabled && minChild.index === maxChild.index) {
        squishedIndices[minChild.index] = true;
        clusterId = -2;
        nsquished++;
      } else {
        clusterNumber++;
        clusterId = clusterNumber;
      }
      for (var j = minChild.index; j <= maxChild.index; j++) {
        clusterIdVector.setValue(j, clusterId);
      }

    }
    this.$label.text((clusterNumber) + ' cluster'
      + phantasus.Util.s(clusterNumber));
    if (nsquished > 0) {
      this.$squishedLabel.text(nsquished + ' squished');
    } else {
      this.$squishedLabel.text('');
    }
    if (squishEnabled) {
      this.positions.setSquishedIndices(squishedIndices);
    }
    if (this.heatMap.getTrackIndex(clusterIdVector.getName(),
        this.type === phantasus.AbstractDendrogram.Type.COLUMN) === -1) {
      var settings = {
        discrete: true,
        discreteAutoDetermined: true,
        display: ['color']
      };

      this.heatMap.addTrack(clusterIdVector.getName(),
        this.type === phantasus.AbstractDendrogram.Type.COLUMN,
        settings);
    }

    if (this.type === phantasus.AbstractDendrogram.Type.COLUMN) {
      this.project.setGroupColumns([new phantasus.SortKey(clusterIdVector.getName(), phantasus.SortKey.SortOrder.UNSORTED)], true);
    } else {
      this.project.setGroupRows([new phantasus.SortKey(clusterIdVector.getName(), phantasus.SortKey.SortOrder.UNSORTED)], true);
    }
  },
  dispose: function () {
    phantasus.AbstractCanvas.prototype.dispose.call(this);
    this.$label.remove();
    this.$squishedLabel.remove();
    this.hammer.off('panend', this.panend).off('panstart',
      this.panstart).off('panmove', this.panmove).off('tap', this.tap);
    this.hammer.destroy();
    this.$label = null;
    this.$squishedLabel = null;
  },
  isCut: function () {
    return this.cutHeight < this.tree.maxHeight;
  },
  getMinIndex: function () {
    return 0;
  },
  getMaxIndex: function () {
    return this.positions.getLength() - 1;
  },
  getNode: function (p) {
    var _this = this;
    if (this.lastNode) {
      var xy = _this.toPix(this.lastNode);
      if (Math.abs(xy[0] - p.x) < 4 && Math.abs(xy[1] - p.y) < 4) {
        return this.lastNode;
      }
    }
    this.lastNode = this._getNode(p);
    return this.lastNode;
  },
  // getNode : function(p) {
  // var x = p.x;
  // var y = p.y;
  // var leafIndex = this.positions.getIndex(x, true);
  // if (leafIndex >= 0 && leafIndex < leafNodeIds.length) {
  // leafid = leafNodeIds[leafIndex];
  // } else {
  // return null;
  // }
  // var n = leafNodes.get(leafid);
  // if (n != null) {
  // while (!n.isRoot()) {
  // var parent = n.getParent();
  // getNodePosition(parent, p);
  // if (Math.abs(p.x - x) < 4 && Math.abs(p.y - y) < 4) {
  // return parent;
  // }
  // n = parent;
  // }
  // }
  // return null;
  // },
  _getNode: function (p) {
    var _this = this;
    // brute force search
    var hit = null;
    try {
      phantasus.DendrogramUtil.dfs(this.tree.rootNode, function (node) {
        var xy = _this.toPix(node);
        if (Math.abs(xy[0] - p.x) < 4 && Math.abs(xy[1] - p.y) < 4) {
          hit = node;
          throw 'break';
        }
        return hit === null;
      });
    }
    catch (x) {
      // break of out dfs
    }
    return hit;
  },
  getResizeCursor: function () {
    if (this.type === phantasus.AbstractDendrogram.Type.COLUMN) {
      return 'ns-resize';
    } else if (this.type === phantasus.AbstractDendrogram.Type.ROW) {
      return 'ew-resize';
    }
    return 'nesw-resize';
  },
  isDragHotSpot: function (p) {
    return false;
  },
  preDraw: function (context, clip) {
  },
  postDraw: function (context, clip) {
  },
  prePaint: function (clip, context) {
    this.scale = this.createScale();
    var min = this.getMinIndex(clip);
    var max = this.getMaxIndex(clip);
    if (min !== this.lastMinIndex || max !== this.lastMinIndex) {
      this.lastMinIndex = min;
      this.lastMaxIndex = max;
    }
    this.invalid = true;
  },
  draw: function (clip, context) {
    context.translate(-clip.x, -clip.y);
    context.strokeStyle = 'black';
    context.fillStyle = 'black';
    this.scale = this.createScale();
    var min = this.lastMinIndex;
    var max = this.lastMaxIndex;
    context.lineWidth = this.lineWidth;
    this.preDraw(context, clip);
    context.strokeStyle = this.defaultStroke;
    context.fillStyle = 'rgba(166,206,227,0.5)';
    this.drawDFS(context, this.tree.rootNode, min, max, 0);
    context.strokeStyle = 'black';
    context.fillStyle = 'black';
    this.postDraw(context, clip);
  },
  /**
   * @abstract
   */
  drawCutSlider: function () {
    throw new Error();
  },
  postPaint: function (clip, context) {
    context.strokeStyle = 'black';
    this.paintMouseOver(clip, context);
    this.drawCutSlider(clip, context);
    // this.drawHighlightedPathsToRoot(context, this.lastMinIndex,
    // this.lastMaxIndex);
  },
  // drawHighlightedPathsToRoot : function(context, minIndex, maxIndex) {
  // context.lineWidth = 1;
  // context.strokeStyle = 'black';
  // context.textAlign = 'left';
  // var i = 0;
  // for ( var key in this.nodeIdToHighlightedPathsToRoot) {
  // context.fillStyle = '#99d594';
  // context.strokeStyle = context.fillStyle;
  // var node = this.nodeIdToHighlightedPathsToRoot[key];
  // if (node.collapsed) {
  // for (var node = node.parent; node.collapsedChildren != null; node =
  // node.parent) {
  // node = node.parent;
  // }
  // }
  // // var pix = this.toPix(node);
  // // context.globalAlpha = 0.5;
  // // context.beginPath();
  // // context.arc(pix[0], pix[1], 8, Math.PI * 2, false);
  // // context.fill();
  // // context.globalAlpha = 1;
  // for (var root = node; root.parent !== undefined; root = root.parent) {
  // this
  // .drawPathFromNodeToParent(context, root, minIndex,
  // maxIndex);
  // }
  // i++;
  // }
  // },
  getNodeRadius: function (node) {
    // if (this._nodeRadiusScaleField != null) {
    // var vals = node.info[this._nodeRadiusScaleField];
    // if (vals === undefined) {
    // return 4;
    // }
    // // TODO get max or min
    // return this._nodeRadiusScale(vals[0]) * 8;
    // }
    return 4;
  },

  drawNode: function (context, node) {
  },
  drawDFS: function (context, node, minIndex, maxIndex) {
    if (this.type !== phantasus.AbstractDendrogram.Type.RADIAL) {
      if ((node.maxIndex < minIndex) || (node.minIndex > maxIndex)) {
        return;
      }
    }
    var nodeFill = this.getNodeFill(node);
    if (nodeFill !== undefined) {
      context.fillStyle = nodeFill;
      this.drawNode(context, node);
    }
    context.strokeStyle = this.getPathStroke(node);
    var children = node.children;
    if (children !== undefined) {
      this.drawNodePath(context, node, minIndex, maxIndex);
      for (var i = 0, nchildren = children.length; i < nchildren; i++) {
        this.drawDFS(context, children[i], minIndex, maxIndex);
      }

    }
  }
};

phantasus.Util.extend(phantasus.AbstractDendrogram, phantasus.AbstractCanvas);
phantasus.Util.extend(phantasus.AbstractDendrogram, phantasus.Events);

/**
 * Action object contains
 * @param options.which Array of key codes
 * @param options.shift Whether shift key is required
 * @param options.commandKey Whether command key is required
 * @param options.name Shortcut name
 * @param options.cb Function callback
 * @param options.accept Additional function to test whether to accept shortcut
 * @param options.icon Optional icon to display
 */
phantasus.ActionManager = function () {
  this.actionNameToAction = new phantasus.Map();
  this.actions = [];
  // TODO copy all row/column metadata
  // pin/unpin tab,
  // header stuff-display, delete.
  this.add({
    ellipsis: false,
    name: 'Sort/Group',
    cb: function (options) {
      new phantasus.SortDialog(options.heatMap.getProject());
    },
    icon: 'fa fa-sort-alpha-asc'
  });

  var $filterModal = null;
  this.add({
    name: 'Filter',
    ellipsis: false,
    cb: function (options) {
      if ($filterModal == null) {
        var filterModal = [];
        var filterLabelId = _.uniqueId('phantasus');
        filterModal
          .push('<div class="modal" tabindex="1" role="dialog" aria-labelledby="'
            + filterLabelId + '">');
        filterModal.push('<div class="modal-dialog" role="document">');
        filterModal.push('<div class="modal-content">');
        filterModal.push('<div class="modal-header">');
        filterModal
          .push('<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>');
        filterModal.push('<h4 class="modal-title" id="' + filterLabelId
          + '">Filter</h4>');
        filterModal.push('</div>');
        filterModal.push('<div class="modal-body"></div>');
        filterModal.push('<div class="modal-footer"><button type="button" class="btn btn-default" data-dismiss="modal">Close</button></div>');
        filterModal.push('</div>');
        filterModal.push('</div>');
        filterModal.push('</div>');
        $filterModal = $(filterModal.join(''));
        $filterModal.on('mousewheel', function (e) {
          e.stopPropagation();
        });
        var $filter = $('<div style="padding-bottom:30px;"></div>');
        $filter.appendTo($filterModal.find('.modal-body'));
        var filterHtml = [];
        filterHtml
          .push('<div class="radio"><label><input type="radio" name="rowsOrColumns" value="rows" checked>Rows</label></div> ');
        filterHtml
          .push('<div class="radio"><label><input type="radio" name="rowsOrColumns" value="columns">Columns</label></div>');

        var $filterChooser = $(filterHtml.join(''));
        $filterChooser.appendTo($filter);
        var columnFilterUI = new phantasus.FilterUI(options.heatMap.getProject(), true);
        var rowFilterUI = new phantasus.FilterUI(options.heatMap.getProject(), false);
        // options.heatMap.getProject().getRowFilter().on('focus', function (e) {
        //   $filterChooser.find('[value=rows]').prop('checked', true);
        //   columnFilterUI.$div.hide();
        //   rowFilterUI.$div.show();
        //   $filterModal.modal('show');
        //   phantasus.Util.trackEvent({
        //     eventCategory: '',
        //     eventAction: 'rowFilter'
        //   });
        //
        // });
        // options.heatMap.getProject().getColumnFilter().on('focus', function (e) {
        //   $filterChooser.find('[value=columns]').prop('checked', true);
        //   columnFilterUI.$div.show();
        //   rowFilterUI.$div.hide();
        //   $filterModal.modal('show');
        //   phantasus.Util.trackEvent({
        //     eventCategory: '',
        //     eventAction: 'columnFilter'
        //   });
        // });
        rowFilterUI.$div.appendTo($filter);
        columnFilterUI.$div.appendTo($filter);
        columnFilterUI.$div.css('display', 'none');
        var $filterRadio = $filterChooser.find('[name=rowsOrColumns]');
        $filterRadio.on('change', function (e) {
          var val = $filterRadio.filter(':checked').val();
          if (val === 'columns') {
            columnFilterUI.$div.show();
            rowFilterUI.$div.hide();
          } else {
            columnFilterUI.$div.hide();
            rowFilterUI.$div.show();
          }
          e.preventDefault();
        });
        $filterModal.appendTo(options.heatMap.$content);
        $filterModal.on('hidden.bs.modal', function () {
          options.heatMap.focus();
        });
      }
      $filterModal.modal('show');
    },
    icon: 'fa fa-filter'
  });

  this.add({
    name: 'Options',
    ellipsis: false,
    cb: function (options) {
      options.heatMap.showOptions();
    },
    icon: 'fa fa-cog'
  });

  this.add({
    which: [191], // slash
    commandKey: true,
    global: true,
    name: 'Toggle Search',
    cb: function (options) {
      options.heatMap.getToolbar().toggleSearch();
    }
  });

  //
  this.add({
    name: 'Copy Image',
    icon: 'fa fa-clipboard',
    cb: function (options) {
      var bounds = options.heatMap.getTotalSize();
      var height = bounds.height;
      var width = bounds.width;
      var canvas = $('<canvas></canvas>')[0];
      var backingScale = phantasus.CanvasUtil.BACKING_SCALE;
      canvas.height = backingScale * height;
      canvas.style.height = height + 'px';
      canvas.width = backingScale * width;
      canvas.style.width = width + 'px';
      var context = canvas.getContext('2d');
      phantasus.CanvasUtil.resetTransform(context);
      options.heatMap.snapshot(context);
      var url = canvas.toDataURL();
      // canvas.toBlob(function (blob) {
      // 	url = URL.createObjectURL(blob);
      // 	event.clipboardData
      // 	.setData(
      // 		'text/html',
      // 		'<img src="' + url + '">');
      // });

      phantasus.Util.setClipboardData([
        {
        format: 'text/html',
        data: '<img src="' + url + '">'
      }], true);
    }
  });

  //
  this.add({
    name: 'Close Tab',
    cb: function (options) {
      options.heatMap.getTabManager().remove(options.heatMap.tabId);
    }
  });
  this.add({
    name: 'Rename Tab',
    ellipsis: false,
    cb: function (options) {
      options.heatMap.getTabManager().rename(options.heatMap.tabId);
    }
  });

  this.add({
    which: [88], // x
    commandKey: true,
    name: 'New Heat Map',
    accept: function (options) {
      return (!options.isInputField || window.getSelection().toString() === '');
    },

    cb: function (options) {
      phantasus.HeatMap.showTool(new phantasus.NewHeatMapTool(),
        options.heatMap);
    }
  });

  this.add({
    name: 'Submit to Shiny GAM',
    cb: function (options) {
      phantasus.HeatMap.showTool(new phantasus.shinyGamTool(), options.heatMap);
    },
    icon: 'fa fa-share-square-o'
  });

  this.add({
    name: 'Submit to Enrichr',
    cb: function (options) {
      new phantasus.enrichrTool(
        options.heatMap.getProject()
      );
    },
    icon: 'fa fa-table'
  });

  this.add({
    name: 'GSEA plot',
    cb: function (options) {
      new phantasus.gseaTool(
        options.heatMap.getProject()
      );
    }
  });

  this.add({
    which: [67], // C
    commandKey: true,
    name: 'Copy'
  });

  this.add({
    which: [86], // V
    commandKey: true,
    name: 'Paste Dataset'
  });

  this.add({
    global: true,
    name: 'Open',
    ellipsis: false,
    cb: function (options) {
      phantasus.HeatMap.showTool(new phantasus.OpenFileTool(), options.heatMap);
    },
    which: [79],
    commandKey: true,
    icon: 'fa fa-folder-open-o'
  });

  this.add({
    ellipsis: false,
    name: 'Save Image',
    gui: function () {
      return new phantasus.SaveImageTool();
    },
    cb: function (options) {
      phantasus.HeatMap.showTool(this.gui(),
        options.heatMap);
    },
    which: [83],
    commandKey: true,
    global: true,
    icon: 'fa fa-file-image-o'
  });

  this.add({
    ellipsis: false,
    name: 'Save Dataset',
    gui: function () {
      return new phantasus.SaveDatasetTool();
    },
    cb: function (options) {
      phantasus.HeatMap.showTool(this.gui(),
        options.heatMap);
    },
    // shiftKey: true,
    // which: [83],
    // commandKey: true,
    // global: true,
    icon: 'fa fa-floppy-o'
  });

  this.add({
    ellipsis: false,
    name: 'Save Session',
    gui: function () {
      return new phantasus.SaveSessionTool();
    },
    cb: function (options) {
      phantasus.HeatMap.showTool(this.gui(), options.heatMap);
    },
    icon: 'fa fa-anchor'
  });

  if (typeof echarts !== 'undefined') {
    this.add({
      name: 'Chart',
      cb: function (options) {
        new phantasus.ChartTool({
          project: options.heatMap.getProject(),
          heatmap: options.heatMap,
          getVisibleTrackNames: _.bind(
            options.heatMap.getVisibleTrackNames, options.heatMap)
        });
      },
      icon: 'fa fa-line-chart'
    });
  }
  if (typeof Plotly !== 'undefined') {
    this.add({
      name: 'PCA Plot',
      cb: function (options) {
        new phantasus.PcaPlotTool({
          project: options.heatMap.getProject()
        });
      },
      icon: 'fa fa-line-chart'
    });
  }

  this.add({
    name: 'Zoom In',
    cb: function (options) {
      options.heatMap.zoom(true);
    },
    which: [107, 61, 187]
  });
  this.add({
    name: 'Zoom Out',
    cb: function (options) {
      options.heatMap.zoom(false);
    },
    which: [173, 189, 109]
  });

  this.add({
    name: 'Fit To Window',
    cb: function (options) {
      options.heatMap.fitToWindow({fitRows: true, fitColumns: true, repaint: true});
    },
    which: [48], // zero
    commandKey: true,
    icon: 'fa fa-compress'
  });
  this.add({
    name: 'Fit Columns To Window',
    cb: function (options) {
      options.heatMap.fitToWindow({fitRows: false, fitColumns: true, repaint: true});
    }
  });
  this.add({
    name: 'Fit Rows To Window',
    cb: function (options) {
      options.heatMap.fitToWindow({fitRows: true, fitColumns: false, repaint: true});
    }
  });
  this.add({
    name: '100%',
    cb: function (options) {
      options.heatMap.resetZoom();
    },
    button: '100%'
  });

  this.add({
    which: [35],
    name: 'Go To End',
    cb: function (options) {
      options.heatMap.scrollLeft(options.heatMap.heatmap.getPreferredSize().width);
      options.heatMap.scrollTop(options.heatMap.heatmap.getPreferredSize().height);
    }
  });
  this.add({
    which: [36], // home key
    name: 'Go To Start',
    cb: function (options) {
      options.heatMap.scrollLeft(0);
      options.heatMap.scrollTop(0);
    }
  });
  this.add({
    which: [34], // page down
    commandKey: true,
    name: 'Go To Bottom',
    cb: function (options) {
      options.heatMap
        .scrollTop(options.heatMap.heatmap.getPreferredSize().height);
    }
  });
  this.add({
    which: [34], // page down
    commandKey: false,
    name: 'Scroll Page Down',
    cb: function (options) {
      var pos = options.heatMap.scrollTop();
      options.heatMap.scrollTop(pos + options.heatMap.heatmap.getUnscaledHeight()
        - 2);
    }
  });

  this.add({
    which: [33], // page up
    commandKey: true,
    name: 'Go To Top',
    cb: function (options) {
      options.heatMap
        .scrollTop(0);
    }
  });
  this.add({
    which: [33], // page up
    commandKey: false,
    name: 'Scroll Page Up',
    cb: function (options) {
      var pos = options.heatMap.scrollTop();
      options.heatMap.scrollTop(pos - options.heatMap.heatmap.getUnscaledHeight()
        + 2);
    }
  });

  this.add({
    which: [38], // up arrow
    commandKey: true,
    name: 'Zoom Out Rows',
    cb: function (options) {
      options.heatMap.zoom(false, {
        columns: false,
        rows: true
      });
    }
  });
  this.add({
    which: [38], // up arrow
    commandKey: false,
    name: 'Scroll Up',
    cb: function (options) {
      options.heatMap.scrollTop(options.heatMap.scrollTop() - 8);
    }
  });

  this.add({
    which: [40], // down arrow
    commandKey: true,
    name: 'Zoom In Rows',
    cb: function (options) {
      options.heatMap.zoom(true, {
        columns: false,
        rows: true
      });
    }
  });
  this.add({
    which: [40], // down arrow
    commandKey: false,
    name: 'Scroll Down',
    cb: function (options) {
      options.heatMap.scrollTop(options.heatMap.scrollTop() + 8);
    }
  });

  this.add({
    which: [37], // left arrow
    commandKey: true,
    name: 'Zoom Out Columns',
    cb: function (options) {
      options.heatMap.zoom(false, {
        columns: true,
        rows: false
      });
    }
  });
  this.add({
    which: [37], // left arrow
    commandKey: false,
    name: 'Scroll Left',
    cb: function (options) {
      options.heatMap.scrollLeft(options.heatMap.scrollLeft() - 8);
    }
  });

  this.add({
    which: [39], // right arrow
    commandKey: true,
    name: 'Zoom In Columns',
    cb: function (options) {
      options.heatMap.zoom(true, {
        columns: true,
        rows: false
      });
    }
  });
  this.add({
    which: [39], // right arrow
    commandKey: false,
    name: 'Scroll Right',
    cb: function (options) {
      options.heatMap.scrollLeft(options.heatMap.scrollLeft() + 8);
    }
  });
  this.add({
    name: 'Tutorial',
    cb: function () {
      window
        .open('phantasus-tutorial.html');
    }
  });
  this.add({
    icon: 'fa fa-code',
    name: 'Source Code',
    cb: function () {
      window.open('https://github.com/ctlab/phantasus');
    }
  });
  var $findModal;
  var $search;

  this.add({
    which: [65],
    ellipsis: false,
    shiftKey: true,
    commandKey: true,
    name: 'Search Menus',
    cb: function (options) {
      if ($findModal == null) {
        var findModal = [];
        var id = _.uniqueId('phantasus');
        findModal
          .push('<div class="modal" tabindex="1" role="dialog" aria-labelledby="'
            + id + '">');
        findModal.push('<div class="modal-dialog" role="document">');
        findModal.push('<div class="modal-content">');
        findModal.push('<div class="modal-header">');
        findModal
          .push('<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>');
        findModal.push('<h4 class="modal-title" id="' + id
          + '">Enter action</h4>');
        findModal.push('</div>');
        findModal.push('<div class="modal-body ui-front"><input class="form-control input-sm"></div>');
        findModal.push('</div>');
        findModal.push('</div>');
        findModal.push('</div>');
        $findModal = $(findModal.join(''));
        $findModal.appendTo(options.heatMap.$content);
        var allActions = options.heatMap.getActionManager().getActions();
        $search = $findModal.find('input');
        $search.on('keyup', function (e) {
          if (e.which === 13) {
            var text = $search.val().trim();
            if (text !== '') {
              var action = _this.getAction(text);
              if (action) {
                $findModal.modal('hide');
                _this.execute(text, {event: e});
              }
            }
          }
        });
        phantasus.Util.autosuggest({
          $el: $search,
          multi: false,
          suggestWhenEmpty: false,
          //  history: options.history,
          filter: function (tokens, response) {
            var token = tokens[0].trim();
            var matches = [];
            var replaceRegex = new RegExp('(' + phantasus.Util.escapeRegex(token) + ')', 'i');
            for (var i = 0; i < allActions.length; i++) {
              if (allActions[i].cb) {
                var name = allActions[i].name;
                if (replaceRegex.test(name)) {
                  matches.push({
                    clear: true,
                    value: name,
                    label: '<span style="margin-left: 10px">'
                    + name.replace(replaceRegex, '<b>$1</b>') + '</span>'
                  });
                }
              }
            }
            response(matches);

          },
          select: function () {
            setTimeout(function () {
              var text = $search.val().trim();
              if (text !== '') {
                var action = _this.getAction(text);
                if (action) {
                  $findModal.modal('hide');
                  _this.execute(text);
                }
              }
            }, 20);

          }
        });
        $findModal.on('hidden.bs.modal', function () {
          options.heatMap.focus();
        });
      }
      $findModal.modal('show');
      $search.focus();
    }
  });
  this.add({
    name: 'Keyboard Shortcuts',
    cb: function (options) {
      new phantasus.HeatMapKeyListener(options.heatMap).showKeyMapReference();
    }
  });

  /*this.add({
    name: 'Linking',
    cb: function () {
      window
        .open('/linking.html');
    }
  });*/
  this.add({
    name: 'Contact',
    icon: 'fa fa-envelope-o',
    cb: function (options) {
      phantasus.FormBuilder.showInModal({
        title: 'Contact',
        html: 'Please email us at alsergbox@gmail.com',
        focus: options.heatMap.getFocusEl()
      });
    }
  });

  this.add({
    which: [65], // a
    commandKey: true,
    name: 'Select All',
    accept: function (options) {
      var active = options.heatMap.getActiveComponent();
      return (active === 'rowTrack' || active === 'columnTrack');
    },
    cb: function (options) {
      var active = options.heatMap.getActiveComponent();
      var selectionModel = active === 'rowTrack' ? options.heatMap.getProject()
        .getRowSelectionModel() : options.heatMap.getProject()
        .getColumnSelectionModel();
      var count = active === 'rowTrack' ? options.heatMap.getProject()
        .getSortedFilteredDataset().getRowCount() : options.heatMap
        .getProject().getSortedFilteredDataset()
        .getColumnCount();
      var indices = new phantasus.Set();
      for (var i = 0; i < count; i++) {
        indices.add(i);
      }
      selectionModel.setViewIndices(indices, true);
    }
  });

  var invertAction = function (options, isColumns) {
    var model = isColumns ? options.heatMap.getProject().getColumnSelectionModel() : options.heatMap.getProject().getRowSelectionModel();
    var viewIndices = model.getViewIndices();
    var inverse = new phantasus.Set();
    var n = n = isColumns ? options.heatMap.getProject().getSortedFilteredDataset().getColumnCount() : options.heatMap.getProject().getSortedFilteredDataset().getRowCount();
    for (var i = 0; i < n; i++) {
      if (!viewIndices.has(i)) {
        inverse.add(i);
      }
    }
    model.setViewIndices(inverse, true);
  };
  this.add({
    name: 'Invert Selected Rows',
    cb: function (options) {
      invertAction(options, false);
    }
  });
  this.add({
    name: 'Invert Selected Columns',
    cb: function (options) {
      invertAction(options, true);
    }
  });
  var clearAction = function (options, isColumns) {
    var model = isColumns ? options.heatMap.getProject()
      .getColumnSelectionModel() : options.heatMap.getProject()
      .getRowSelectionModel();
    model.setViewIndices(new phantasus.Set(), true);
  };
  this.add({
    name: 'Clear Selected Rows',
    cb: function (options) {
      clearAction(options, false);
    }
  });
  this.add({
    name: 'Clear Selected Columns',
    cb: function (options) {
      clearAction(options, true);
    }
  });

  var moveToTop = function (options, isColumns) {
    var project = options.heatMap.getProject();
    var selectionModel = !isColumns ? project.getRowSelectionModel()
      : project
        .getColumnSelectionModel();
    var viewIndices = selectionModel.getViewIndices().values();
    if (viewIndices.length === 0) {
      return;
    }
    viewIndices.sort(function (a, b) {
      return (a === b ? 0 : (a < b ? -1 : 1));
    });
    var converter = isColumns ? project.convertViewColumnIndexToModel
      : project.convertViewRowIndexToModel;
    converter = _.bind(converter, project);
    var modelIndices = [];
    for (var i = 0, n = viewIndices.length; i < n; i++) {
      modelIndices.push(converter(viewIndices[i]));
    }
    var sortKey = new phantasus.MatchesOnTopSortKey(project, modelIndices, 'selection on top', isColumns);
    sortKey.setLockOrder(1);
    sortKey.setUnlockable(false);
    if (isColumns) {
      project
      .setColumnSortKeys(
        phantasus.SortKey
        .keepExistingSortKeys(
          [sortKey],
          project
          .getColumnSortKeys().filter(function (key) {
            return !(key instanceof phantasus.MatchesOnTopSortKey && key.toString() === sortKey.toString());
          })),
        true);
    } else {
      project
      .setRowSortKeys(
        phantasus.SortKey
        .keepExistingSortKeys(
          [sortKey],
          project
          .getRowSortKeys().filter(function (key) {
            return !(key instanceof phantasus.MatchesOnTopSortKey && key.toString() === sortKey.toString());
          })),
        true);
    }
  };
  this.add({
    name: 'Move Selected Rows To Top',
    cb: function (options) {
      moveToTop(options, false);
    }
  });
  this.add({
    name: 'Move Selected Columns To Top',
    cb: function (options) {
      moveToTop(options, true);
    }
  });
  var selectAll = function (options, isColumns) {
    var project = options.heatMap.getProject();
    var selectionModel = !isColumns ? project.getRowSelectionModel()
      : project
        .getColumnSelectionModel();
    var count = !isColumns ? project
      .getSortedFilteredDataset()
      .getRowCount() : project
      .getSortedFilteredDataset()
      .getColumnCount();
    var indices = new phantasus.Set();
    for (var i = 0; i < count; i++) {
      indices.add(i);
    }
    selectionModel.setViewIndices(indices, true);
  };
  this.add({
    name: 'Select All Rows',
    cb: function (options) {
      selectAll(options, false);
    }
  });
  this.add({
    name: 'Select All Columns',
    cb: function (options) {
      selectAll(options, true);
    }
  });
  var copySelection = function (options, isColumns) {
    var project = options.heatMap.getProject();
    var dataset = project
      .getSortedFilteredDataset();
    var activeTrackName = options.heatMap.getSelectedTrackName(isColumns);
    var v;
    if (activeTrackName == null) {
      v = isColumns ? dataset.getColumnMetadata()
        .get(0) : dataset
        .getRowMetadata().get(0);
    } else {
      v = isColumns ? dataset.getColumnMetadata()
        .getByName(activeTrackName) : dataset
        .getRowMetadata().getByName(activeTrackName);
    }

    var selectionModel = isColumns ? project
      .getColumnSelectionModel() : project
      .getRowSelectionModel();
    var text = [];
    var toStringFunction = phantasus.VectorTrack.vectorToString(v);
    selectionModel.getViewIndices().forEach(
      function (index) {
        text.push(toStringFunction(v
          .getValue(index)));
      });
    phantasus.Util.setClipboardData([
      {
      format: 'text/plain',
      data: text.join('\n')
    }]);
  };
  this.add({
    name: 'Copy Selected Rows',
    cb: function (options) {
      copySelection(options, false);
    }
  });
  this.add({
    name: 'Copy Selected Columns',
    cb: function (options) {
      copySelection(options, true);
    }
  });

  var annotateSelection = function (options, isColumns) {

    var project = options.heatMap.getProject();
    var selectionModel = isColumns ? project
        .getColumnSelectionModel()
      : project
        .getRowSelectionModel();
    if (selectionModel.count() === 0) {
      phantasus.FormBuilder
        .showMessageModal({
          title: 'Annotate Selection',
          html: 'No ' + (isColumns ? 'columns' : 'rows') + ' selected.',
          focus: options.heatMap.getFocusEl()
        });
      return;
    }
    var formBuilder = new phantasus.FormBuilder();
    formBuilder.append({
      name: 'annotation_name',
      type: 'text',
      required: true
    });
    formBuilder.append({
      name: 'annotation_value',
      type: 'text',
      required: true
    });
    phantasus.FormBuilder
      .showOkCancel({
        title: 'Annotate',
        content: formBuilder.$form,
        focus: options.heatMap.getFocusEl(),
        okCallback: function () {
          var value = formBuilder
            .getValue('annotation_value');
          var annotationName = formBuilder
            .getValue('annotation_name');
          var dataset = project
            .getSortedFilteredDataset();
          var fullDataset = project
            .getFullDataset();
          if (isColumns) {
            dataset = phantasus.DatasetUtil
              .transposedView(dataset);
            fullDataset = phantasus.DatasetUtil
              .transposedView(fullDataset);
          }

          var existingVector = fullDataset
            .getRowMetadata()
            .getByName(
              annotationName);
          var v = dataset
            .getRowMetadata().add(
              annotationName);

          selectionModel
            .getViewIndices()
            .forEach(
              function (index) {
                v
                  .setValue(
                    index,
                    value);
              });
          phantasus.VectorUtil
            .maybeConvertStringToNumber(v);
          project
            .trigger(
              'trackChanged',
              {
                vectors: [v],
                display: existingVector != null ? []
                  : [phantasus.VectorTrack.RENDER.TEXT],
                columns: isColumns
              });
        }
      });
  };
  this.add({
    ellipsis: false,
    name: 'Annotate Selected Rows',
    cb: function (options) {
      annotateSelection(options, false);
    }
  });
  this.add({
    ellipsis: false,
    name: 'Annotate Selected Columns',
    cb: function (options) {
      annotateSelection(options, true);
    }
  });
  this.add({
    name: 'Copy Selected Dataset',
    cb: function (options) {
      var project = options.heatMap.getProject();
      var dataset = project.getSelectedDataset({
        emptyToAll: false
      });
      var columnMetadata = dataset
        .getColumnMetadata();
      var rowMetadata = dataset.getRowMetadata();
      // only copy visible tracks
      var visibleColumnFields = options.heatMap
        .getVisibleTrackNames(true);
      var columnFieldIndices = [];
      _.each(visibleColumnFields, function (name) {
        var index = phantasus.MetadataUtil.indexOf(
          columnMetadata, name);
        if (index !== -1) {
          columnFieldIndices.push(index);
        }
      });
      columnMetadata = new phantasus.MetadataModelColumnView(
        columnMetadata, columnFieldIndices);
      var rowMetadata = dataset.getRowMetadata();
      // only copy visible tracks
      var visibleRowFields = options.heatMap
        .getVisibleTrackNames(false);
      var rowFieldIndices = [];
      _.each(visibleRowFields, function (name) {
        var index = phantasus.MetadataUtil.indexOf(
          rowMetadata, name);
        if (index !== -1) {
          rowFieldIndices.push(index);
        }
      });
      rowMetadata = new phantasus.MetadataModelColumnView(
        rowMetadata, rowFieldIndices);

      var text = new phantasus.GctWriter()
        .write(dataset);
      phantasus.Util.setClipboardData([
        {
        format: 'text/plain',
        data: text
      }]);

    }
  });
  var _this = this;
  //console.log(_this);
  [
    new phantasus.HClusterTool(), new phantasus.MarkerSelection(),
    new phantasus.NearestNeighbors(), new phantasus.AdjustDataTool(),
    new phantasus.CollapseDatasetTool(), new phantasus.CreateAnnotation(), new phantasus.SimilarityMatrixTool(),
    new phantasus.TransposeTool(), new phantasus.TsneTool(), new phantasus.DevAPI(),
    new phantasus.KmeansTool(), new phantasus.LimmaTool()].forEach(function (tool) {
    _this.add({
      ellipsis: false,
      name: tool.toString(),
      gui: function () {
        return tool;
      },
      cb: function (options) {
        phantasus.HeatMap.showTool(tool, options.heatMap);
      }
    });
  });
  this.add({
    name: 'Edit Fonts',
    ellipse: true,
    cb: function (options) {
      var trackInfo = options.heatMap.getLastSelectedTrackInfo();
      var project = options.heatMap.getProject();
      var model = trackInfo.isColumns ? project
        .getColumnFontModel() : project
        .getRowFontModel();
      var chooser = new phantasus.FontChooser({fontModel: model, track: options.heatMap.getTrack(trackInfo.name, trackInfo.isColumns), heatMap: options.heatMap});
      phantasus.FormBuilder.showInModal({
        title: 'Edit Fonts',
        html: chooser.$div,
        close: 'Close',
        focus: options.heatMap.getFocusEl()
      });
    }
  });

};
phantasus.ActionManager.prototype = {
  getActions: function () {
    return this.actions;
  },
  getAction: function (name) {
    return this.actionNameToAction.get(name);
  },
  execute: function (name, args) {
    var action = this.getAction(name);
    if (args == null) {
      args = {};
    }

    args.heatMap = this.heatMap;
    action.cb(args);

    phantasus.Util.trackEvent({
      eventCategory: 'Tool',
      eventAction: name
    });
  },
  add: function (action) {
    this.actions.push(action);
    this.actionNameToAction.set(action.name, action);
  }
};

phantasus.CanvasUtil = function () {
};
phantasus.CanvasUtil.dragging = false;

phantasus.CanvasUtil.FONT_NAME = '"Helvetica Neue",Helvetica,Arial,sans-serif';
phantasus.CanvasUtil.FONT_COLOR = 'rgb(0, 0, 0)';
phantasus.CanvasUtil.getFontFamily = function (context) {
  // older versions of Adobe choke when a font family contains a font that is not installed
  return (typeof C2S !== 'undefined' && context instanceof C2S) || (typeof canvas2pdf !== 'undefined' && context instanceof canvas2pdf.PdfContext)
    ? 'Helvetica'
    : phantasus.CanvasUtil.FONT_NAME;
};
phantasus.CanvasUtil.getPreferredSize = function (c) {
  var size = c.getPreferredSize();
  var prefWidth = c.getPrefWidth();
  var prefHeight = c.getPrefHeight();
  // check for override override
  if (prefWidth !== undefined) {
    size.widthSet = true;
    size.width = prefWidth;
  }
  if (prefHeight !== undefined) {
    size.heightSet = true;
    size.height = prefHeight;
  }
  return size;
};
phantasus.CanvasUtil.BACKING_SCALE = 1;
if (typeof window !== 'undefined' && 'devicePixelRatio' in window) {
  if (window.devicePixelRatio > 1) {
    phantasus.CanvasUtil.BACKING_SCALE = window.devicePixelRatio;
  }
}

phantasus.CanvasUtil.setBounds = function (canvas, bounds) {
  var backingScale = phantasus.CanvasUtil.BACKING_SCALE;

  if (bounds.height != null) {
    canvas.height = bounds.height * backingScale;
    canvas.style.height = bounds.height + 'px';
  }
  if (bounds.width != null) {
    canvas.width = bounds.width * backingScale;
    canvas.style.width = bounds.width + 'px';
  }
  if (bounds.left != null) {
    canvas.style.left = bounds.left + 'px';
  }
  if (bounds.top != null) {
    canvas.style.top = bounds.top + 'px';
  }
};

phantasus.CanvasUtil.drawShape = function (context, shape, x, y, size2, isFill) {
  if (size2 < 0) {
    return;
  }
  context.beginPath();
  if (shape === 'circle-minus') {
    context.arc(x, y, size2, 0, 2 * Math.PI, false);
    context.moveTo(x - size2, y);
    context.lineTo(x + size2, y);
  } else if (shape === 'circle') {
    context.arc(x, y, size2, 0, 2 * Math.PI, false);
  } else if (shape === 'square') {
    context.rect(x - size2, y - size2, size2 * 2, size2 * 2);
  } else if (shape === 'plus') {
    // vertical line
    context.moveTo(x, y - size2);
    context.lineTo(x, y + size2);
    // horizontal line
    context.moveTo(x - size2, y);
    context.lineTo(x + size2, y);
  } else if (shape === 'x') {
    context.moveTo(x - size2, y - size2);
    context.lineTo(x + size2, y + size2);
    context.moveTo(x + size2, y - size2);
    context.lineTo(x - size2, y + size2);
  } else if (shape === 'asterisk') {
    // x with vertical line
    context.moveTo(x - size2, y - size2);
    context.lineTo(x + size2, y + size2);
    context.moveTo(x + size2, y - size2);
    context.lineTo(x - size2, y + size2);

    context.moveTo(x, y - size2);
    context.lineTo(x, y + size2);
  } else if (shape === 'diamond') {
    // start at middle top
    context.moveTo(x, y - size2);
    // right
    context.lineTo(x + size2, y);
    // bottom
    context.lineTo(x, y + size2);
    // left
    context.lineTo(x - size2, y);
    // top
    context.lineTo(x, y - size2);
  } else if (shape === 'triangle-up') {
    // top
    context.moveTo(x, y - size2);
    // right
    context.lineTo(x + size2, y + size2);
    // left
    context.lineTo(x - size2, y + size2);
    context.lineTo(x, y - size2);
  } else if (shape === 'triangle-down') {
    // bottom
    context.moveTo(x, y + size2);
    // left
    context.lineTo(x - size2, y - size2);
    // right
    context.lineTo(x + size2, y - size2);
    context.lineTo(x, y + size2);
  } else if (shape === 'triangle-left') {
    // left
    context.moveTo(x - size2, y);
    // top
    context.lineTo(x + size2, y - size2);
    // bottom
    context.lineTo(x + size2, y + size2);
    context.lineTo(x - size2, y);
  } else if (shape === 'triangle-right') {
    // right
    context.moveTo(x + size2, y);
    // lower left
    context.lineTo(x - size2, y + size2);

    // upper left
    context.lineTo(x - size2, y - size2);
    context.lineTo(x + size2, y);
  }
  isFill ? context.fill() : context.stroke();

};
phantasus.CanvasUtil.drawLine = function (context, x1, y1, x2, y2) {
  context.beginPath();
  context.moveTo(x1, y1);
  context.lineTo(x2, y2);
  context.stroke();
};
phantasus.CanvasUtil.resetTransform = function (context) {
  context.setTransform(1, 0, 0, 1, 0, 0);
  if (phantasus.CanvasUtil.BACKING_SCALE !== 1) {
    context.scale(phantasus.CanvasUtil.BACKING_SCALE,
      phantasus.CanvasUtil.BACKING_SCALE);
  }
};
phantasus.CanvasUtil.bezierCurveTo = function (context, start, end) {
  var m1 = (start[1] + end[1]) / 2;
  context.beginPath();
  context.moveTo(start[0], start[1]);
  // context.lineTo(leftp[0], leftp[1]);
  context.bezierCurveTo(start[0], m1, end[0], m1, end[0], end[1]);
  context.stroke();
};
phantasus.CanvasUtil.createCanvas = function () {
  var $c = $('<canvas></canvas>');
  $c.attr('tabindex', '0');
  $c.css({
    cursor: 'default',
    outline: 0,
    overflow: 'hidden',
    position: 'absolute',
    'z-index': 1
  });
  return $c[0];
};
phantasus.CanvasUtil.getHeaderStringWidth = function (context, s) {
  context.font = '14px ' + phantasus.CanvasUtil.getFontFamily(context);
  return context.measureText(s).width + 18;
};

phantasus.CanvasUtil.forceSubPixelRendering = function (context) {
  context.getImageData(0, 0, 1, 1);
};
phantasus.CanvasUtil.getVectorStringWidth = function (context, vector, positions,
                                                     end) {
  if (positions.getSize() < 6) {
    return 0;
  }
  var fontSize = Math.min(phantasus.VectorTrack.MAX_FONT_SIZE, positions.getSize() - 2);
  if (fontSize <= 0) {
    return 0;
  }
  context.font = fontSize + 'px ' + phantasus.CanvasUtil.getFontFamily(context);
  var toString = phantasus.VectorTrack.vectorToString(vector);
  var maxWidth = 0;
  // var maxWidth2 = 0;
  var n = end <= 0 ? vector.size() : Math.min(end, vector.size());
  for (var i = 0; i < n; i++) {
    var value = vector.getValue(i);
    if (value != null && value != '') {
      value = toString(value);
    } else {
      continue;
    }
    var width = context.measureText(value).width;
    if (width > maxWidth) {
      maxWidth = width;
    }
    // if (width > maxWidth2 && width < maxWidth) {
    // maxWidth2 = width;
    // }
  }
  return maxWidth === 0 ? maxWidth : (maxWidth + 2);
};
phantasus.CanvasUtil.clipString = function (context, string, availTextWidth) {
  var textWidth = context.measureText(string).width;
  if (textWidth <= availTextWidth) {
    return string;
  }
  var clipString = '...';
  availTextWidth -= context.measureText(clipString).width;
  if (availTextWidth <= 0) {
    // can not fit any characters
    return clipString;
  }
  var width = 0;
  for (var nChars = 0, stringLength = string.length; nChars < stringLength; nChars++) {
    width += context.measureText(string[nChars]).width;
    if (width > availTextWidth) {
      string = string.substring(0, nChars);
      break;
    }
  }
  return string + clipString;
};
phantasus.CanvasUtil.toSVG = function (drawable, file) {
  var totalSize = {
    width: drawable.getWidth(),
    height: drawable.getHeight()
  };
  var context = new C2S(totalSize.width, totalSize.height);
  context.save();
  drawable.draw({
    x: 0,
    y: 0,
    width: totalSize.width,
    height: totalSize.height
  }, context);
  context.restore();
  var svg = context.getSerializedSvg();
  var blob = new Blob([svg], {
    type: 'text/plain;charset=utf-8'
  });
  saveAs(blob, file);
};
phantasus.CanvasUtil.getMousePos = function (element, event, useDelta) {
  return phantasus.CanvasUtil.getMousePosWithScroll(element, event, 0, 0,
    useDelta);
};

phantasus.CanvasUtil.getClientXY = function (event, useDelta) {
  var clientX;
  var clientY;
  if (event.pointers) {
    if (event.pointers.length > 0) {
      clientX = event.pointers[0].clientX - (useDelta ? event.deltaX : 0);
      clientY = event.pointers[0].clientY - (useDelta ? event.deltaY : 0);
    } else {
      clientX = event.srcEvent.clientX - (useDelta ? event.deltaX : 0);
      clientY = event.srcEvent.clientY - (useDelta ? event.deltaY : 0);
    }
  } else {
    clientX = event.clientX;
    clientY = event.clientY;
  }
  return {
    x: clientX,
    y: clientY
  };
};
phantasus.CanvasUtil.getMousePosWithScroll = function (element, event, scrollX,
                                                      scrollY, useDelta) {
  return phantasus.CanvasUtil._getMousePosWithScroll(element, scrollX,
    scrollY, phantasus.CanvasUtil.getClientXY(event, useDelta));
};

phantasus.CanvasUtil._getMousePosWithScroll = function (element, scrollX,
                                                       scrollY, clientXY) {
  var rect = element.getBoundingClientRect();
  return {
    x: clientXY.x - rect.left + scrollX,
    y: clientXY.y - rect.top + scrollY
  };
};

/**
 * @param {phantasus.Set} [] -
 *            options.set set of selected items
 * @see phantasus.Table
 */
phantasus.CheckBoxList = function (options) {
  var _this = this;
  var set = options.set || new phantasus.Set();
  options = $.extend(true, {}, {
    height: '150px',
    showHeader: false,
    select: false,
    search: true,
    checkBoxSelectionOnTop: false,
    rowHeader: function (item) {
      var header = [];
      // header
      // .push('<div style="overflow: hidden;text-overflow: ellipsis;"
      // class="phantasus-hover">');
      header.push('<span><input name="toggle" type="checkbox" '
        + (set.has(_this.getter(item)) ? ' checked' : '') + '/> ');
      header.push('</span>');
      // header
      // .push('<button
      // style="background-color:inherit;position:absolute;top:0;right:0;line-height:inherit;padding:0px;margin-top:4px;"
      // class="btn btn-link phantasus-hover-show">only</button>');
      // header.push('</div>');
      return header.join('');
      // return '<span><input name="toggle"
      // type="checkbox" '
      // + (set.has(_this.getter(item)) ? ' checked' : '')
      // + '/> </span>'
    }
  }, options);
  options = phantasus.Table.createOptions(options);
  if (options.columns.length === 1) {
    options.maxWidth = 583;
  }
  var idColumn = options.columns[0];
  for (var i = 0; i < options.columns.length; i++) {
    if (options.columns[i].idColumn) {
      idColumn = options.columns[i];
      break;
    }
  }

  this.getter = idColumn.getter;
  var html = [];

  var table = new phantasus.Table(options);
  if (options.columns.length === 1) {
    options.$el.find('.slick-table-header').find('[name=right]').remove();
  }
  this.table = table;
  var html = [];

  html.push('<div style="display:inline;">');
  html.push('<div style="display:inline;" class="dropdown">');
  html.push('<button class="btn btn-default btn-xs dropdown-toggle" type="button"' +
    ' data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">');
  html.push('<i data-name="checkbox" class="fa fa-square-o"' +
    ' aria-hidden="true"></i>');
  html.push(' <span class="fa fa-caret-down"></span>');
  html.push('</button>');
  html.push('<ul style="font-size:12px;" class="dropdown-menu">');
  html.push('<li><a name="selectAll" href="#">Select All</a></li>');
  html.push('<li><a name="selectNone" href="#">Select None</a></li>');
  html.push('<li><a name="invertSel" href="#">Invert Selection</a></li>');

  html.push('</ul>');
  html.push('</div>');
  html.push('<span data-name="available" style="font-size:12px;padding-left:6px;"></span>');
  html.push('</div>');
  var $checkBoxEl = $(html.join(''));
  table.$header.find('[name=left]').html($checkBoxEl);
  var $selection = $checkBoxEl.find('[data-name=available]');
  var $selectAll = $checkBoxEl.find('[name=selectAll]');
  var $selectNone = $checkBoxEl.find('[name=selectNone]');
  var $cb = $checkBoxEl.find('[data-name=checkbox]');
  var updateLabel = function () {
    var label = [];
    label.push('selected ');
    label.push(phantasus.Util.intFormat(set.size()));
    label.push(' of ');
    label.push(phantasus.Util.intFormat(table.getAllItemCount()));
    if (table.getFilteredItemCount() !== table.getAllItemCount()) {
      label.push(', ');
      label.push(phantasus.Util.intFormat(table.getFilteredItemCount()));
      label.push(table.getFilteredItemCount() === 1 ? ' match' : ' matches');
    }
    $selection.html(label.join(''));

  };
  table.grid.on('filter', function (e) {
    updateLabel();
  });
  $cb.on('click', function (e) {
    if ($cb.hasClass('fa-square-o')) {
      var items = table.getItems(); // select all
      for (var i = 0; i < items.length; i++) {
        set.add(_this.getter(items[i]));
      }
    } else { // select none
      var items = table.getItems();
      for (var i = 0; i < items.length; i++) {
        set.remove(_this.getter(items[i]));
      }
    }
    table.trigger('checkBoxSelectionChanged', {
      source: _this,
      set: set
    });
    e.preventDefault();
    e.stopPropagation();

  });
  $selectAll.on('click', function (e) {
    var items = table.getItems();
    for (var i = 0, nitems = items.length; i < nitems; i++) {
      set.add(_this.getter(items[i]));
    }
    _this.table.trigger('checkBoxSelectionChanged', {
      source: _this,
      set: set
    });
    e.preventDefault();
    _this.table.redraw();
  });
  $checkBoxEl.find('[name=invertSel]').on('click', function (e) {
    // selected become unselected, unselected become selected
    var items = table.getItems();
    for (var i = 0, nitems = items.length; i < nitems; i++) {
      var val = _this.getter(items[i]);
      if (set.has(val)) {
        set.remove(val);
      } else {
        set.add(val);
      }

    }
    _this.table.trigger('checkBoxSelectionChanged', {
      source: _this,
      set: set
    });
    e.preventDefault();
    _this.table.redraw();
  });
  $selectNone.on('click', function (e) {
    var items = table.getItems();
    for (var i = 0, nitems = items.length; i < nitems; i++) {
      set.remove(_this.getter(items[i]));
    }
    _this.table.trigger('checkBoxSelectionChanged', {
      source: _this,
      set: set
    });

    e.preventDefault();
    _this.table.redraw();
  });

  this.set = set;
  this.table = table;
  updateLabel();

  var priorCount = 0;
  this.table.on('checkBoxSelectionChanged', function () {
    if (set.size() === 0) {
      $cb.attr('class', 'fa fa-square-o');
    } else {
      var items = table.getItems();
      var count = 0;
      var found = false;
      var notFound = false;
      for (var i = 0; i < items.length; i++) {
        if (set.has(_this.getter(items[i]))) {
          count++;
          found = true;
          if (notFound) {
            break;
          }
        } else {
          notFound = true;
          if (found) {
            break;
          }
        }
      }
      if (count === 0) {
        $cb.attr('class', 'fa fa-square-o');
      } else if (count === items.length) {
        $cb.attr('class', 'fa fa-check-square-o');
      } else {
        $cb.attr('class', 'fa fa-minus-square-o');
      }
    }

    updateLabel();

    _this.table.redraw();
  });

  table.on('click',
    function (e) {
      var $target = $(e.target);
      var item = table.getItems()[e.row];
      var value = _this.getter(item);
      if ($target.is('.phantasus-hover-show')) { // only
        set.clear();
        set.add(value);
        _this.table.trigger('checkBoxSelectionChanged', {
          source: _this,
          set: set
        });
      } else if (!options.select
        || ($target.is('[type=checkbox]') && $target
          .attr('name') === 'toggle')) {
        if (set.has(value)) {
          set.remove(value);
        } else {
          set.add(value);
        }
        _this.table.trigger('checkBoxSelectionChanged', {
          source: _this,
          set: set
        });
      }

    });

};
phantasus.CheckBoxList.prototype = {
  searchWithPredicates: function (predicates) {
    this.table.searchWithPredicates(predicates);
  },
  autocomplete: function (tokens, cb) {
    this.table.autocomplete(tokens, cb);
  },
  setHeight: function (height) {
    this.table.setHeight(height);
  },
  resize: function () {
    this.table.resize();
  },
  setSearchVisible: function (visible) {
    this.table.setSearchVisible(visible);
  },
  getSelectedRows: function () {
    return this.table.getSelectedRows();
  },
  getSelectedItems: function () {
    return this.table.getSelectedItems();
  },
  setSelectedRows: function (rows) {
    this.table.setSelectedRows(rows);
  },
  getItems: function (items) {
    return this.table.getItems();
  },
  getAllItemCount: function () {
    return this.table.getAllItemCount();
  },
  getFilteredItemCount: function () {
    return this.table.getFilteredItemCount();
  },
  setFilter: function (f) {
    this.table.setFilter(f);
  },

  redraw: function () {
    this.table.redraw();
  },
  getSelection: function () {
    return this.set;
  },
  clearSelection: function (values) {
    this.set.clear();
    this.table.redraw();
  },
  setValue: function (values) {
    this.setSelectedValues(values);
  },
  setSelectedValues: function (values) {
    this.set.clear();

    if (phantasus.Util.isArray(values)) {
      for (var i = 0; i < values.length; i++) {
        this.set.add(values[i]);
      }
    } else {
      this.set.add(values);
    }
    this.table.redraw();
  },
  val: function () {
    return this.set.values();
  },
  on: function (evtStr, handler) {
    this.table.on(evtStr, handler);
    return this;
  },
  off: function (evtStr, handler) {
    this.table.off(evtStr, handler);
  },
  setItems: function (items) {
    // remove items in selection that are not in new items
    var newItems = new phantasus.Set();
    var getter = this.getter;
    for (var i = 0; i < items.length; i++) {
      newItems.add(getter(items[i]));

    }
    var selection = this.set;
    selection.forEach(function (val) {
      if (!newItems.has(val)) {
        selection.remove(val);
      }
    });

    this.table.setItems(items);
    this.table.trigger('checkBoxSelectionChanged', {
      source: this,
      set: selection
    });
  }
};

/**
 *
 * @param options.colorModel
 * @param options.track
 * @param options.heatMap
 * @constructor
 */
phantasus.ColorSchemeChooser = function (options) {
  var colorModel = options.colorModel;
  var track = options.track;
  var heatMap = options.heatMap;
  // ensure map exists
  colorModel.getMappedValue(track.getVector(track.settings.colorByField), track.getVector(track.settings.colorByField).getValue(0));
  var formBuilder = new phantasus.FormBuilder();
  if (track.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_COLOR)) {
    formBuilder.append({
      value: track.settings.colorByField != null,
      type: 'checkbox',
      name: 'use_another_annotation_to_determine_color'
    });
    var annotationNames = phantasus.MetadataUtil.getMetadataNames(
      track.isColumns ? heatMap.getProject().getFullDataset().getColumnMetadata() : heatMap.getProject().getFullDataset().getRowMetadata());
    annotationNames.splice(annotationNames.indexOf(track.getName()), 1);
    formBuilder.append({
      name: 'annotation_name',
      type: 'bootstrap-select',
      options: annotationNames,
      search: annotationNames.length > 10,
      value: track.settings.colorByField
    });
  }
  formBuilder.append({
    name: 'discrete',
    type: 'checkbox',
    value: track.getVector(track.settings.colorByField).getProperties().get(phantasus.VectorKeys.DISCRETE)
  });
  var dataType = phantasus.VectorUtil.getDataType(track.getVector(track.settings.colorByField));
  var isNumber = dataType === 'number' || dataType === '[number]';
  formBuilder.setVisible('discrete', isNumber);
  formBuilder.setVisible('annotation_name', track.settings.colorByField != null);

  var $chooser = $('<div></div>');
  $chooser.appendTo(formBuilder.$form);
  var updateChooser = function () {
    var colorSchemeChooser;
    var v = track
      .getVector(track.settings.colorByField);
    formBuilder.setValue('discrete', v.getProperties().get(phantasus.VectorKeys.DISCRETE));
    if (v.getProperties().get(phantasus.VectorKeys.DISCRETE)) {
      colorModel.getMappedValue(v, v.getValue(0)); // make sure color map exists
      colorSchemeChooser = new phantasus.DiscreteColorSchemeChooser(
        {
          colorScheme: {
            scale: colorModel
              .getDiscreteColorScheme(track
                .getVector(track.settings.colorByField))
          }
        });
      colorSchemeChooser.on('change', function (event) {
        colorModel.setMappedValue(track
            .getVector(track.settings.colorByField), event.value,
          event.color);
        track.setInvalid(true);
        track.repaint();
      });
    } else {
      colorModel.getContinuousMappedValue(v, v.getValue(0)); // make sure color map exists
      colorSchemeChooser = new phantasus.HeatMapColorSchemeChooser(
        {
          showRelative: false
        });

      colorSchemeChooser
        .setColorScheme(colorModel
          .getContinuousColorScheme(v));
      colorSchemeChooser.on('change', function (event) {
        track.setInvalid(true);
        track.repaint();
      });
    }
    $chooser.html(colorSchemeChooser.$div);
    track.setInvalid();
    track.repaint();
  };
  formBuilder.find('use_another_annotation_to_determine_color').on('change', function () {
    var checked = $(this).prop('checked');
    formBuilder.setValue('annotation_name', null);
    formBuilder.setVisible('annotation_name', checked);
    if (!checked) {
      track.settings.colorByField = null;
      updateChooser();
    } else {
      $chooser.empty();
    }

    formBuilder.setVisible('discrete', false);
  });
  formBuilder.find('annotation_name').on('change', function () {
    var annotationName = $(this).val();
    // ensure map exists
    colorModel.getMappedValue(track.getVector(annotationName), track.getVector(annotationName).getValue(0));
    track.settings.colorByField = annotationName;
    var dataType = phantasus.VectorUtil.getDataType(track.getVector(track.settings.colorByField));
    var isNumber = dataType === 'number' || dataType === '[number]';
    formBuilder.setVisible('discrete', isNumber);
    updateChooser();
    track.setInvalid(true);
    track.repaint();
  });

  formBuilder.find('discrete').on('change', function () {
    track.getVector(track.settings.colorByField).getProperties().set(phantasus.VectorKeys.DISCRETE, $(this).prop('checked'));
    updateChooser();
    track.setInvalid(true);
    track.repaint();
  });
  updateChooser();
  this.$div = formBuilder.$form;
};

phantasus.ColumnDendrogram = function (heatMap, tree, positions, project) {
  phantasus.AbstractDendrogram.call(this, heatMap, tree, positions,
    project, phantasus.AbstractDendrogram.Type.COLUMN);
};
phantasus.ColumnDendrogram.prototype = {
  drawNode: function (context, node) {
    var radius = this.getNodeRadius(node);
    var pix = this.toPix(node);
    context.beginPath();
    context.arc(pix[0], pix[1], 4, Math.PI * 2, false);
    context.fill();
  },
  isDragHotSpot: function (p) {
    return Math.abs(this.scale(this.cutHeight) - p.y) <= 2;
  },
  drawCutSlider: function (clip, context) {
    if (context.setLineDash) {
      context.setLineDash([5]);
    }
    context.strokeStyle = 'black';
    var ny = this.scale(this.cutHeight);
    context.beginPath();
    context.moveTo(clip.x, ny);
    context.lineTo(this.getUnscaledWidth(), ny);
    context.stroke();
    if (context.setLineDash) {
      context.setLineDash([]);
    }
  },
  createScale: function () {
    // root has the largest height, leaves the smallest height
    return d3.scale.linear().domain([this.tree.maxHeight, 0]).range(
      [0, this.getUnscaledHeight()]);
  },
  paintMouseOver: function (clip, context) {
    if (this.project.getHoverColumnIndex() !== -1) {
      phantasus.CanvasUtil.resetTransform(context);
      context.translate(-clip.x, 0);
      this.drawColumnBorder(context, this.positions, this.project
        .getHoverColumnIndex(), this.getUnscaledWidth());
    }
  },
  drawColumnBorder: function (context, positions, index, gridSize) {
    var size = positions.getItemSize(index);
    var pix = positions.getPosition(index);
    // top and bottom lines
    context.beginPath();
    context.moveTo(pix + size, 0);
    context.lineTo(pix + size, gridSize);
    context.stroke();
    context.beginPath();
    context.moveTo(pix, 0);
    context.lineTo(pix, gridSize);
    context.stroke();
  },
  getMaxIndex: function (clip) {
    return phantasus.Positions.getRight(clip, this.positions);
  },
  getMinIndex: function (clip) {
    return phantasus.Positions.getLeft(clip, this.positions);
  },
  getPreferredSize: function (context) {
    return {
      width: Math.ceil(this.positions.getPosition(this.positions
            .getLength() - 1)
        + this.positions
          .getItemSize(this.positions.getLength() - 1)),
      height: 100
    };
  },
  toPix: function (node) {
    var min = this.positions.getPosition(node.minIndex)
      + this.positions.getItemSize(node.minIndex) / 2;
    var max = this.positions.getPosition(node.maxIndex)
      + this.positions.getItemSize(node.maxIndex) / 2;
    return [(min + max) / 2, this.scale(node.height)];
  },
  drawPathFromNodeToParent: function (context, node) {
    var pix = this.toPix(node);
    var parentPix = this.toPix(node.parent);
    context.beginPath();
    context.moveTo(pix[0], pix[1]);
    context.lineTo(pix[0], parentPix[1]);
    context.lineTo(parentPix[0], parentPix[1]);
    context.stroke();
  },
  drawNodePath: function (context, node, minIndex, maxIndex) {
    var children = node.children;
    var left = children[0];
    var right = children[1];
    // set up points for poly line
    var ny = this.scale(node.height);
    var rx = this.toPix(right)[0];
    var ry = this.scale(right.height);
    var lx = this.toPix(left)[0];
    var ly = this.scale(left.height);
    var x, y;
    if (!this.drawLeafNodes) {
      var leftIsLeaf = left.children !== undefined;
      var rightIsLeaf = right.children !== undefined;
      if (leftIsLeaf) {
        ly = ny + 4;
      }
      if (rightIsLeaf) {
        ry = ny + 4;
      }
      x = [rx, rx, lx, lx];
      y = [ry, ny, ny, ly];
    } else {
      x = [rx, rx, lx, lx];
      y = [ry, ny, ny, ly];
    }
    context.beginPath();
    context.moveTo(x[0], y[0]);
    for (var i = 1, length = x.length; i < length; i++) {
      context.lineTo(x[i], y[i]);
    }
    context.stroke();
  }
};
phantasus.Util.extend(phantasus.ColumnDendrogram, phantasus.AbstractDendrogram);

phantasus.ConditionalRenderingUI = function (heatmap) {
  var _this = this;
  this.heatmap = heatmap;
  var $div = $('<div class="container-fluid" style="min-width:180px;"></div>');
  $div.on('click', '[data-name=add]', function (e) {
    var $this = $(this);
    var $row = $this.closest('.phantasus-entry');
    // add after
    var index = $row.index();
    var condition = {
      seriesName: null,
      color: 'rgb(0,0,0)',
      shape: null,
      inheritColor: true,
      accept: function (val) {
        return false;
      }

    };

    heatmap.heatmap.getColorScheme().getConditions().insert(index,
      condition);

    $row.after(_this.add(condition));
    e.preventDefault();
  });
  $div.on('click', '[data-name=delete]', function (e) {
    var $this = $(this);
    var $row = $this.closest('.phantasus-entry');
    var index = $row.index() - 1;
    heatmap.heatmap.getColorScheme().getConditions().remove(index);
    heatmap.revalidate();
    $row.remove();
    e.preventDefault();
  });
  var html = [];
  html
  .push('<div class="phantasus-entry">');
  html.push('<div class="row">');
  html
    .push('<div style="padding-bottom:20px;" class="col-xs-8"><a class="btn btn-default btn-xs"' +
      ' role="button"' +
      ' data-name="add" href="#">Add Condition</a></div>');

  html.push('</div>');
  html.push('</div>');

  $div.append(html.join(''));
  this.$div = $div;
  heatmap.heatmap.getColorScheme().getConditions().getConditions().forEach(
    function (c) {
      _this.add(c).appendTo($div);
    });

};

phantasus.ConditionalRenderingUI.prototype = {
  add: function (condition) {
    var _this = this;
    // shape: shapes and line
    // color: if no color cell is drawn using this shape, otherwise draw
    // shape on top of cell
    // seriesName name
    // value >= x and <= x
    var html = [];
    html.push('<div style="border-top:1px solid LightGrey;padding-bottom:6px;padding-top:6px;"' +
      ' class="phantasus-entry">');
    html.push('<form class="form-horizontal">');
    // seriesName
    html.push('<div class="form-group">');
    html
      .push('<label class="col-xs-2">Series</label>');
    html.push('<div class="col-xs-6">');
    html
      .push('<select class="form-control phantasus-form-control-inline" name="cond_series">');
    html.push(phantasus.Util.createOptions(phantasus.DatasetUtil
      .getSeriesNames(this.heatmap.getProject().getFullDataset())));
    html.push('</select>');
    html.push('</div>');
    html.push('</div>');

    // condition
    html.push('<div class="form-group">');
    html.push('<label class="col-xs-2">Condition</label>');
    html.push('<div class="col-xs-6">');
    html
      .push('<select class="form-control phantasus-form-control-inline" name="lower"><option value="gte">&gt;=</option><option value="gt">&gt;</option></select>');
    html
      .push('<input class="form-control phantasus-form-control-inline" name="v1" size="5" type="text">');
    html.push('<span style="margin-right:1em;">and</span>');
    html
      .push('<select class="form-control phantasus-form-control-inline" name="upper"><option value="lte">&lt;=</option><option value="lt">&lt;</option></select>');
    html
      .push('<input class="form-control phantasus-form-control-inline" name="v2" size="5" type="text">');
    html.push('</div>');
    html.push('</div>');

    // shape
    html.push('<div class="form-group">');
    html.push('<label class="col-xs-2">Shape</label>');
    var shapeField = new phantasus.ShapeField({shapes: phantasus.VectorShapeModel.FILLED_SHAPES, showNone: false});
    html.push('<div class="col-xs-4">');
    html.push('<div style="display:inline;" data-name="shapeHolder"></div>');
    html.push('</div>');
    html.push('</div>');

    // color
    html.push('<div class="form-group">');
    html.push('<label class="col-xs-offset-2 col-xs-4"><input name="inherit_color"' +
      ' type="checkbox" checked> Inherit' +
      ' color</label>');
    html.push('</div>');

    html.push('<div class="form-group">');
    html.push('<label class="col-xs-2">Color</label>');
    html.push('<div class="col-xs-4">');
    html
      .push('<input class="form-control" type="color" name="color" style="display:inline;' +
        ' width:6em;" disabled>');
    html.push('</div>');
    html.push('</div>');

    html.push('<div class="row"><div class="col-xs-11">');
    html
      .push('<a class="btn btn-default btn-xs" role="button" data-name="delete"' +
        ' href="#">Delete Condition</a>');
    html.push('</div></div>');
    html.push('</div>'); // phantasus-entry
    var $el = $(html.join(''));
    console.log($el.find('form').length);
    $el.find('form').on('submit', function (e) {
      e.preventDefault();
    });
    shapeField.$el.appendTo($el.find('[data-name=shapeHolder]'));
    var $color = $el.find('[name=color]');
    var $series = $el.find('[name=cond_series]');
    var $v1 = $el.find('[name=v1]');
    var $v2 = $el.find('[name=v2]');
    var $v1Op = $el.find('[name=lower]');
    var $v2Op = $el.find('[name=upper]');
    var $inherit_color = $el.find('[name=inherit_color]');
    $color.prop('disabled', condition.inheritColor);
    $color.val(condition.color);
    $series.val(condition.seriesName);
    shapeField.setShapeValue(condition.shape);
    if (condition.v1 != null && !isNaN(condition.v1)) {
      $v1.val(condition.v1);
    }
    if (condition.v2 != null && !isNaN(condition.v2)) {
      $v2.val(condition.v2);
    }
    $v1Op.val(condition.v1Op);
    $v2Op.val(condition.v2Op);

    function updateAccept() {
      var v1 = parseFloat($($v1).val());
      var v2 = parseFloat($($v2).val());
      var v1Op = $v1Op.val();
      var v2Op = $v2Op.val();
      condition.v1 = v1;
      condition.v2 = v2;
      condition.v1Op = v1Op;
      condition.v2Op = v2Op;
      var gtf = function () {
        return true;
      };
      var ltf = function () {
        return true;
      };
      if (!isNaN(v1)) {
        gtf = v1Op === 'gt' ? function (val) {
          return val > v1;
        } : function (val) {
          return val >= v1;
        };
      }

      if (!isNaN(v2)) {
        ltf = v2Op === 'lt' ? function (val) {
          return val < v2;
        } : function (val) {
          return val <= v2;
        };
      }
      condition.accept = function (val) {
        return gtf(val) && ltf(val);
      };
      _this.heatmap.revalidate();
    }

    $v1Op.on('change', function (e) {
      updateAccept();

    });
    $v2Op.on('change', function (e) {
      updateAccept();
    });
    $v1.on('keyup', _.debounce(function (e) {
      updateAccept();
    }, 100));
    $v2.on('keyup', _.debounce(function (e) {
      updateAccept();
    }, 100));
    $inherit_color.on('click', function (e) {
      condition.inheritColor = $(this).prop('checked');
      $color.prop('disabled', condition.inheritColor);
      _this.heatmap.revalidate();
    });
    $color.on('change', function (e) {
      condition.color = $(this).val();
      _this.heatmap.revalidate();
    });
    shapeField.on('change', function (e) {
      condition.shape = e.shape;
      _this.heatmap.revalidate();
    });
    $series.on('change', function (e) {
      condition.seriesName = $(this).val();
      _this.heatmap.revalidate();
    });
    condition.seriesName = $series.val();
    return $el;

  }
};

phantasus.DendrogramUtil = {};
phantasus.DendrogramUtil.setIndices = function (root, counter) {
  counter = counter || 0;
  var setIndex = function (node) {
    var children = node.children;
    var maxIndex = children[0].maxIndex;
    var minIndex = children[0].minIndex;
    var sum = children[0].index;
    for (var i = 1, length = children.length; i < length; i++) {
      var child = children[i];
      sum += child.index;
      minIndex = Math.min(minIndex, child.minIndex);
      maxIndex = Math.max(maxIndex, child.maxIndex);
    }
    node.minIndex = minIndex;
    node.maxIndex = maxIndex;
    node.index = sum / children.length;
    node.children.sort(function (a, b) {
      return (a.index === b.index ? 0 : (a.index < b.index ? -1 : 1));
    });
  };

  var visit = function (node, callback) {
    var children = node.children;
    var n;
    if (children && (n = children.length)) {
      var i = -1;
      while (++i < n) {
        visit(children[i], callback);
      }
    }
    callback(node);
  };
  visit(root, function (n) {
    if (n.children === undefined) {
      n.minIndex = counter;
      n.maxIndex = counter;
      n.index = counter;
      counter++;
    } else {
      setIndex(n);
    }
    return true;
  });
};
phantasus.DendrogramUtil.convertEdgeLengthsToHeights = function (rootNode) {
  var maxHeight = 0;

  function setHeights(node, height) {
    var newHeight = height;
    if (node.length !== undefined) {
      newHeight += node.length;
    }
    node.height = newHeight;
    maxHeight = Math.max(maxHeight, node.height);
    if (node.children != null) {
      node.children.forEach(function (child) {
        setHeights(child, newHeight);
      });
    }
  }

  setHeights(rootNode, 0);
  var counter = 0;
  phantasus.DendrogramUtil.dfs(rootNode, function (node) {
    node.id = counter;
    counter++;
    node.height = maxHeight - node.height;
    return true;
  });
  return {
    maxHeight: maxHeight,
    n: counter
  };
};
phantasus.DendrogramUtil.writeNewick = function (node, out, leafNodeIdFunction) {
  if (node.children != null && node.children.length > 0) {
    // indent
    out.push('(');
    for (var i = 0; i < node.children.length; i++) {
      if (i > 0) {
        out.push(',');
      }
      phantasus.DendrogramUtil.writeNewick(node.children[i], out, leafNodeIdFunction);
    }
    out.push(')');
  }
  out.push(node.index != null ? leafNodeIdFunction(node) : ''); // leaf nodes have index
  out.push(':');
  var parentHeight = node.parent ? node.parent.height : node.height;
  out.push(parentHeight - node.height);

};
phantasus.DendrogramUtil.parseNewick = function (text) {
  var rootNode = Newick.parse(text);
  var counter = 0;
  var leafNodes = [];

  function visit(node) {
    var children = node.children;
    if (children !== undefined) {
      var left = children[0];
      var right = children[1];
      left.parent = node;
      right.parent = node;
      visit(left);
      visit(right);
    } else { // leaf node
      node.minIndex = counter;
      node.maxIndex = counter;
      node.index = counter;
      leafNodes.push(node);
      counter++;
    }
  }

  visit(rootNode);
  var maxHeight = phantasus.DendrogramUtil.convertEdgeLengthsToHeights(rootNode).maxHeight;
  phantasus.DendrogramUtil.setNodeDepths(rootNode);
  phantasus.DendrogramUtil.setIndices(rootNode);
  return {
    maxHeight: rootNode.height,
    rootNode: rootNode,
    leafNodes: leafNodes,
    nLeafNodes: leafNodes.length
  };
};
phantasus.DendrogramUtil.cutAtHeight = function (rootNode, h) {
  var roots = [];
  phantasus.DendrogramUtil.dfs(rootNode, function (node) {
    if (node.height < h) {
      roots.push(node);
      return false;
    }
    return true;
  });
  roots.sort(function (a, b) {
    return (a.index < b.index ? -1 : (a.index == b.index ? 0 : 1));
  });
  return roots;
};
phantasus.DendrogramUtil.getDeepestChild = function (node, isMin) {
  while (true) {
    if (node.children === undefined) {
      return node;
    }
    var index;
    if (isMin) {
      index = node.children[0].index < node.children[node.children.length - 1].index ? 0
        : node.children.length - 1;
    } else {
      index = node.children[0].index > node.children[node.children.length - 1].index ? 0
        : node.children.length - 1;
    }

    node = node.children[index];
  }
};
/**
 * Pre-order depth first traversal 1. Visit the root. 2. Traverse the left
 * subtree. 3. Traverse the right subtree.
 */
phantasus.DendrogramUtil.dfs = function (node, callback, childrenAccessor) {
  if (childrenAccessor === undefined) {
    childrenAccessor = function (n) {
      return n.children;
    };
  }
  if (callback(node)) {
    var children = childrenAccessor(node);
    var n;
    if (children && (n = children.length)) {
      var i = -1;
      while (++i < n) {
        phantasus.DendrogramUtil.dfs(children[i], callback,
          childrenAccessor);
      }
    }
  }
};
phantasus.DendrogramUtil.copyTree = function (tree) {
  var counter = 0;

  function recurse(node) {
    var children = node.children;
    if (children !== undefined) {
      var newChildren = [];
      for (var i = 0, n = children.length; i < n; i++) {
        var copy = $.extend({}, children[i]);
        copy.parent = node;
        newChildren.push(copy);
      }
      node.children = newChildren;
      for (var i = 0, n = newChildren.length; i < n; i++) {
        recurse(newChildren[i]);
      }
    } else {
      node.index = counter;
      node.minIndex = counter;
      node.maxIndex = counter;
      counter++;
    }
  }

  var rootNode = $.extend({}, tree.rootNode);
  rootNode.parent = undefined;
  recurse(rootNode);
  return {
    nLeafNodes: tree.nLeafNodes,
    maxDepth: tree.maxDepth,
    rootNode: rootNode
  };
};
phantasus.DendrogramUtil.collapseAtDepth = function (rootNode, maxDepth) {
  // restore collapsed children
  phantasus.DendrogramUtil.dfs(rootNode, function (d) {
    if (d.collapsedChildren) {
      d.children = d.collapsedChildren;
      d.collapsedChildren = undefined;
    }
    return true;
  });
  // collapse nodes below specified depth
  phantasus.DendrogramUtil.dfs(rootNode, function (d) {
    var depth = d.depth;
    if (depth > maxDepth) {
      d.collapsedChildren = d.children;
      d.children = undefined;
      return false;
    }
    return true;
  });
};
phantasus.DendrogramUtil.setNodeDepths = function (rootNode) {
  var max = 0;

  function recurse(node, depth) {
    var children = node.children;
    node.depth = depth;
    max = Math.max(depth, max);
    if (children !== undefined) {
      var i = -1;
      var j = depth + 1;
      var n = children.length;
      while (++i < n) {
        var d = recurse(children[i], j);
      }
    }
    return node;
  }

  recurse(rootNode, 0);
  return max;
};
phantasus.DendrogramUtil.sortDendrogram = function (root, vectorToSortBy,
                                                   project, summaryFunction) {
  summaryFunction = summaryFunction || function (array) {
    var min = Number.MAX_VALUE;
    for (var i = 0; i < array.length; i++) {
      // sum += array[i].weight;
      min = Math.min(min, array[i].weight);
    }
    return min;
  };
  var setWeights = function (node) {
    if (node.children !== undefined) {
      var children = node.children;
      for (var i = 0; i < children.length; i++) {
        setWeights(children[i]);
      }
      node.weight = summaryFunction(children);
    } else {
      node.weight = vectorToSortBy.getValue(node.index);
    }
  };
  setWeights(root);
  // sort children by weight
  var nodeIdToModelIndex = {};
  var leafNodes = phantasus.DendrogramUtil.getLeafNodes(root);
  _.each(leafNodes, function (node) {
    nodeIdToModelIndex[node.id] = project.convertViewColumnIndexToModel(node.index);
  });
  phantasus.DendrogramUtil.dfs(root, function (node) {
    if (node.children) {
      node.children.sort(function (a, b) {
        return (a.weight === b.weight ? 0 : (a.weight < b.weight ? -1
          : 1));
      });
    }
    return true;
  });
  phantasus.DendrogramUtil.setIndices(root);
  var sortOrder = [];
  _.each(leafNodes, function (node) {
    var oldModelIndex = nodeIdToModelIndex[node.id];
    var newIndex = node.index;
    sortOrder[newIndex] = oldModelIndex;
  });
  return sortOrder;
};
phantasus.DendrogramUtil.leastCommonAncestor = function (leafNodes) {
  function getPathToRoot(node) {
    var path = new phantasus.Map();
    while (node != null) {
      path.set(node.id, node);
      node = node.parent;
    }
    return path;
  }

  var path = getPathToRoot(leafNodes[0]);
  for (var i = 1; i < leafNodes.length; i++) {
    var path2 = getPathToRoot(leafNodes[i]);
    path.forEach(function (node, id) {
      if (!path2.has(id)) {
        path.remove(id);
      }
    });
    // keep only those in path that are also in path2
  }
  var max = -Number.MAX_VALUE;
  var maxNode;
  path.forEach(function (n, id) {
    if (n.depth > max) {
      max = n.depth;
      maxNode = n;
    }
  });
  return maxNode;
};
// phantasus.DendrogramUtil.computePositions = function(rootNode, positions)
// {
// if (rootNode == null) {
// return;
// }
// phantasus.DendrogramUtil._computePositions(rootNode, positions);
// };
// /**
// * position is (left+right)/2
// */
// phantasus.DendrogramUtil._computePositions = function(node, positions) {
// if (node.children !== undefined) {
// var children = node.children;
// var left = children[0];
// var right = children[1];
// phantasus.DendrogramUtil._computePositions(left, positions);
// phantasus.DendrogramUtil._computePositions(right, positions);
// phantasus.DendrogramUtil.setIndex(node);
// node.position = (left.position + right.position) / 2;
// } else {
// node.position = positions.getItemSize(node.index) / 2
// + positions.getPosition(node.index);
// }
// };

/**
 *
 * @param options.rootNode Dendrogram root node
 * @param options.text Search text
 * @param options.defaultMatchMode
 *            'exact' or 'contains'
 * @param options.matchAllPredicates Whether to match all predicates
 */
phantasus.DendrogramUtil.search = function (options) {
  var searchText = options.text;
  var rootNode = options.rootNode;
  var tokens = phantasus.Util.getAutocompleteTokens(searchText);
  var predicates;
  var nmatches = 0;
  var matchAllPredicates = options.matchAllPredicates === true;

  if (tokens == null || tokens.length == 0) {
    phantasus.DendrogramUtil.dfs(rootNode, function (node) {
      node.search = false;
      return true;
    });
    nmatches = -1;
  } else {
    predicates = phantasus.Util.createSearchPredicates({
      tokens: tokens,
      defaultMatchMode: options.defaultMatchMode
    });
    var npredicates = predicates.length;
    phantasus.DendrogramUtil
      .dfs(
        rootNode,
        function (node) {
          var matches = false;
          if (node.info) {
            searchLabel:
              if (!matchAllPredicates) { // at least one predicate matches
                for (var p = 0; p < npredicates; p++) {
                  var predicate = predicates[p];
                  var filterColumnName = predicate.getField();
                  if (filterColumnName != null) {
                    var vals = node.info[filterColumnName];
                    for (var i = 0, nvals = vals.length; i < nvals; i++) {
                      if (predicate.accept(vals[i])) {
                        matches = true;
                        break searchLabel;
                      }
                    }
                  } else {
                    for (var name in node.info) {
                      var vals = node.info[name];
                      for (var i = 0, nvals = vals.length; i < nvals; i++) {
                        if (predicate.accept(vals[i])) {
                          matches = true;
                          break searchLabel;
                        }
                      }
                    }

                  }
                }
              } else { // all predicates must match
                matches = true;
                for (var p = 0; p < npredicates; p++) {
                  var predicate = predicates[p];
                  var filterColumnName = predicate.getField();
                  if (filterColumnName != null) {
                    var vals = node.info[filterColumnName];
                    for (var i = 0, nvals = vals.length; i < nvals; i++) {
                      if (!predicate.accept(vals[i])) {
                        matches = false;
                        break searchLabel;
                      }
                    }
                  } else {
                    for (var name in node.info) {
                      var vals = node.info[name];
                      for (var i = 0, nvals = vals.length; i < nvals; i++) {
                        if (!predicate.accept(vals[i])) {
                          matches = false;
                          break searchLabel;
                        }
                      }
                    }

                  }
                }
              }
          }
          node.search = matches;
          if (matches) {
            nmatches++;
          }
          return true;
        }
      );

  }
  return nmatches;
}
;
phantasus.DendrogramUtil.squishNonSearchedNodes = function (
  heatMap,
  isColumns) {
  if (isColumns) {
    heatMap.getHeatMapElementComponent().getColumnPositions().setSize(13);
  } else {
    heatMap.getHeatMapElementComponent().getRowPositions().setSize(13);
  }
  var expandedLeafNodes = {};
  var dendrogram = isColumns ? heatMap.columnDendrogram
    : heatMap.rowDendrogram;
  phantasus.DendrogramUtil.dfs(dendrogram.tree.rootNode, function (node) {
    for (var i = node.minIndex; i <= node.maxIndex; i++) {
      if (node.search) {
        expandedLeafNodes[i] = true;
      }
    }
    return true;
  });
  var clusterIds = [];
  var previous = expandedLeafNodes[0];
  var squishedIndices = {};
  if (!previous) {
    squishedIndices[0] = true;
  }
  var clusterNumber = 0;
  clusterIds.push(clusterNumber);
  for (var i = 1, nleaves = dendrogram.tree.leafNodes.length; i < nleaves; i++) {
    var expanded = expandedLeafNodes[i];
    if (expanded !== previous) {
      clusterNumber++;
      previous = expanded;
    }
    if (!expanded) {
      squishedIndices[i] = true;
    }
    clusterIds.push(clusterNumber);
  }
  if (isColumns) {
    heatMap.getHeatMapElementComponent().getColumnPositions().setSquishedIndices(squishedIndices);
    heatMap.getProject().setGroupColumns(
      [new phantasus.SpecifiedGroupByKey(clusterIds)], false);
  } else {
    heatMap.getHeatMapElementComponent().getRowPositions().setSquishedIndices(squishedIndices);
    heatMap.getProject().setGroupRows(
      [new phantasus.SpecifiedGroupByKey(clusterIds)], false);
  }
};
phantasus.DendrogramUtil.getLeafNodes = function (rootNode) {
  var leafNodes = [];
  phantasus.DendrogramUtil.dfs(rootNode, function (node) {
    if (node.children === undefined) {
      leafNodes.push(node);
    }
    return true;
  });
  return leafNodes;
};

phantasus.DiscreteColorSchemeChooser = function (options) {
  var formBuilder = new phantasus.FormBuilder();
  var map = options.colorScheme.scale;

  formBuilder.append({
    name: 'selected_value',
    type: 'bootstrap-select',
    options: map.keys()
  });
  var $select = formBuilder.find('selected_value');
  formBuilder.append({
    style: 'max-width:50px;',
    name: 'selected_color',
    type: 'color'
  });
  var selectedVal = $select.val();
  var _this = this;
  var $color = formBuilder.find('selected_color');
  $color.val(map.get(selectedVal));
  $color.on('change', function (e) {
    var color = $(this).val();
    map.set(selectedVal, color);
    _this.trigger('change', {
      value: selectedVal,
      color: color
    });
  });
  $select.on('change', function () {
    selectedVal = $select.val();
    var c = map.get(selectedVal);
    $color.val(c);
  });
  this.$div = formBuilder.$form;
};
phantasus.DiscreteColorSchemeChooser.prototype = {};
phantasus.Util.extend(phantasus.DiscreteColorSchemeChooser, phantasus.Events);

phantasus.DiscreteColorSupplier = function () {
  this.colorMap = new phantasus.Map();
  this.hiddenValue = 0;
  this.hiddenValues = new phantasus.Set();
  phantasus.AbstractColorSupplier.call(this);
  this.scalingMode = phantasus.HeatMapColorScheme.ScalingMode.FIXED;
};

phantasus.DiscreteColorSupplier.prototype = {
  createInstance: function () {
    return new phantasus.DiscreteColorSupplier();
  },
  /**
   * @param.array Array of name, value, color pairs
   */
  setColorMap: function (array) {
    this.colorMap = new phantasus.Map();
    this.colors = [];
    this.fractions = [];
    this.names = [];
    this.min = Number.MAX_VALUE;
    this.max = -Number.MAX_VALUE;
    for (var i = 0; i < array.length; i++) {
      this.colorMap.set(array[i].value, array[i].color);
      this.fractions.push(array[i].value);
      this.names.push(array[i].name);
      this.colors.push(array[i].color);
      this.min = Math.min(this.min, array[i].value);
      this.max = Math.max(this.max, array[i].value);
    }
  },
  copy: function () {
    var c = this.createInstance();
    c.names = this.names.slice(0);
    c.colorMap = new phantasus.Map();
    this.colorMap.forEach(function (color, value) {
      c.colorMap.set(value, color);
    });
    c.colors = this.colors.slice(0);
    c.fractions = this.fractions.slice(0);
    this.hiddenValues.forEach(function (val) {
      c.hiddenValues.add(val);
    });

    c.missingColor = this.missingColor;
    return c;
  },

  isStepped: function () {
    return true;
  },
  getColor: function (row, column, value) {
    if (this.hiddenValues.has(value)) {
      value = this.hiddenValue;
    }

    if (isNaN(value)) {
      return this.missingColor;
    }
    return this.colorMap.get(value);
  }
};
phantasus.Util.extend(phantasus.DiscreteColorSupplier,
  phantasus.AbstractColorSupplier);

phantasus.Divider = function (vertical) {
  phantasus.AbstractCanvas.call(this, false);
  this.vertical = vertical;
  var that = this;
  var canvas = this.canvas;
  canvas.style.cursor = vertical ? 'ew-resize' : 'ns-resize';

  if (vertical) {
    this.setBounds({
      height: 15,
      width: 4
    });

  } else {
    this.setBounds({
      height: 4,
      width: 15
    });
  }
  this.hammer = phantasus.Util.hammer(canvas, ['pan']).on('panstart',
    this.panstart = function (event) {
      that.trigger('resizeStart');
      phantasus.CanvasUtil.dragging = true;
    }).on('panmove', this.panmove = function (event) {
    if (that.vertical) {
      that.trigger('resize', {
        delta: event.deltaX
      });
    } else {
      that.trigger('resize', {
        delta: event.deltaY
      });
    }
  }).on('panend', this.panend = function (event) {
    phantasus.CanvasUtil.dragging = false;
    that.trigger('resizeEnd');
  });
  this.paint();

};
phantasus.Divider.prototype = {
  dispose: function () {
    phantasus.AbstractCanvas.prototype.dispose.call(this);
    this.hammer.off('panstart', this.panstart).off('panmove', this.panmove).off('panend', this.panend);
    this.hammer.destroy();
  },
  getPreferredSize: function () {
    return {
      width: 3,
      height: this.getUnscaledHeight()
    };
  },
  draw: function (clip, context) {
    var width = this.getUnscaledWidth();
    var height = this.getUnscaledHeight();
    context.clearRect(0, 0, width, height);
    context.strokeStyle = '#ddd';
    if (!this.vertical) {// horizontal line at top
      context.beginPath();
      context.moveTo(0, 1.5);
      context.lineTo(width, 1.5);
      context.stroke();
    } else { // vertical line at left
      context.beginPath();
      context.moveTo(0, 0);
      context.lineTo(0, height);
      context.stroke();
    }
  }
};
phantasus.Util.extend(phantasus.Divider, phantasus.AbstractCanvas);
phantasus.Util.extend(phantasus.Divider, phantasus.Events);

phantasus.DualList = function (leftOptions, rightOptions) {
  var html = [];
  html.push('<div class="container-fluid">');
  html.push('<div class="row">');
  html.push('<div class="col-xs-4"><label>Available Fields</label></div>');
  html.push('<div class="col-xs-2"></div>');
  html.push('<div class="col-xs-4"><label>Selected Fields</label></div>');
  html.push('</div>'); // row
  html.push('<div class="row">');
  html
    .push('<div class="col-xs-4"><select class="form-control" name="left" multiple></select></div>');
  html
    .push('<div class="col-xs-2"><div class="btn-group-vertical" role="group">'
      + '<button name="add" type="button" class="btn btn-xs btn-default">Add</button>'
      + '<button name="remove" type="button" class="btn btn-xs btn-default">Remove</button>'
      + '<button name="up" type="button" class="btn btn-xs btn-default">Move Up</button>'
      + '<button name="down" type="button" class="btn btn-xs btn-default">Move Down</button>'
      + '</div></div>');
  html
    .push('<div class="col-xs-4"><select class="form-control" name="right" multiple></select></div>');
  html.push('</div>'); // row
  html.push('</div>');
  this.$el = $(html.join(''));
  var _this = this;
  this.$el.find('[name=add]').on('click', function () {
    _this.addSelected();
  });
  this.$el.find('[name=remove]').on('click', function () {
    _this.removeSelected();
  });
  this.$el.find('[name=up]').on('click', function () {
    _this.moveUp();
  });
  this.$el.find('[name=down]').on('click', function () {
    _this.moveDown();
  });
  this.left = this.$el.find('[name=left]')[0];
  this.right = this.$el.find('[name=right]')[0];
  for (var i = 0; i < leftOptions.length; i++) {
    this.left.options[i] = leftOptions[i];
  }
  for (var i = 0; i < rightOptions.length; i++) {
    this.right.options[i] = rightOptions[i];
  }
};

phantasus.DualList.prototype = {
  addSelected: function () {
    var left = this.left;
    var right = this.right;
    for (var i = 0; i < left.options.length; i++) {
      if (left.options[i].selected) {
        var opt = left.options[i];
        right.options[right.options.length] = new Option(opt.innerHTML,
          opt.value);
        left.options[i] = null;
        i--;
      }
    }
  },
  addAll: function () {
    var left = this.left;
    var right = this.right;
    for (var i = 0; i < left.options.length; i++) {
      var opt = left.options[i];
      right.options[right.options.length] = new Option(opt.innerHTML,
        opt.value);
    }
    left.options.length = 0;
  },
  removeSelected: function () {
    var left = this.left;
    var right = this.right;
    for (var i = 0; i < right.options.length; i++) {
      if (right.options[i].selected) {
        var opt = right.options[i];
        left.options[left.options.length] = new Option(opt.innerHTML,
          opt.value);
        right.options[i] = null;
        i--;
      }
    }
  },
  getOptions: function (isLeft) {
    var sel = isLeft ? this.left : this.right;
    var options = [];
    for (var i = 0; i < sel.options.length; i++) {
      options.push(sel.options[i].value);
    }
    return options;
  },
  removeAll: function () {
    var left = this.left;
    var right = this.right;
    for (var i = 0; i < right.options.length; i++) {
      var opt = right.options[i];
      left.options[left.options.length] = new Option(opt.innerHTML,
        opt.value);
    }
    right.options.length = 0;
  },
  moveUp: function () {
    var right = this.right;
    var selectedOptions = right.selectedOptions;
    var indices = [];
    for (var i = 0; i < selectedOptions.length; i++) {
      indices.push(selectedOptions[i].index);
    }
    var index = phantasus.Util.indexSort(indices, false);
    for (var i = 0; i < selectedOptions.length; i++) {
      var sel = selectedOptions[index[i]].index;
      var optHTML = right.options[sel].innerHTML;
      var optVal = right.options[sel].value;
      var opt1HTML = right.options[sel - 1].innerHTML;
      var opt1Val = right.options[sel - 1].value;
      right.options[sel] = new Option(opt1HTML, opt1Val);
      right.options[sel - 1] = new Option(optHTML, optVal);
      right.options.selectedIndex = sel - 1;
    }

  },
  moveDown: function () {
    var right = this.right;
    var selectedOptions = right.selectedOptions;
    var indices = [];
    for (var i = 0; i < selectedOptions.length; i++) {
      indices.push(selectedOptions[i].index);
    }
    var index = phantasus.Util.indexSort(indices, false);
    for (var i = 0; i < selectedOptions.length; i++) {
      var sel = selectedOptions[index[i]].index;
      var optHTML = right.options[sel].innerHTML;
      var optVal = right.options[sel].value;
      var opt1HTML = right.options[sel + 1].innerHTML;
      var opt1Val = right.options[sel + 1].value;
      right.options[sel] = new Option(opt1HTML, opt1Val);
      right.options[sel + 1] = new Option(optHTML, optVal);
      right.options.selectedIndex = sel + 1;
    }
  }
};

/**
 *
 * @param options.fileCallback Callback when file is selected
 * @param options.optionsCallback Callback when preloaded option is selected
 * @constructor
 */
phantasus.FilePicker = function (options) {
  var html = [];
  html.push('<div>');
  var myComputer = _.uniqueId('phantasus');
  var url = _.uniqueId('phantasus');
  var googleId = _.uniqueId('phantasus');
  var dropbox = _.uniqueId('phantasus');
  var preloaded = _.uniqueId('phantasus');
  html.push('<ul style="margin-bottom:10px;" class="nav nav-pills phantasus">');
  html.push('<li role="presentation" class="active"><a href="#' + myComputer + '"' +
    ' aria-controls="' + myComputer + '" role="tab" data-toggle="tab"><i class="fa fa-desktop"></i>' +
    ' My Computer</a></li>');
  html.push('<li role="presentation"><a href="#' + url + '"' +
    ' aria-controls="' + url + '" role="tab" data-toggle="tav"><i class="fa fa-link"></i>' +
    ' URL</a></li>');

  if (typeof gapi !== 'undefined') {
    html.push('<li role="presentation"><a href="#' + googleId + '"' +
      ' aria-controls="' + googleId + '" role="tab" data-toggle="tab"><i class="fa' +
      ' fa-google"></i>' +
      ' Google</a></li>');
  }
  if (typeof Dropbox !== 'undefined') {
    html.push('<li role="presentation"><a href="#' + dropbox + '"' +
      ' aria-controls="' + dropbox + '" role="tab" data-toggle="tab"><i class="fa fa-dropbox"></i>' +
      ' Dropbox</a></li>');
  }

  var $sampleDatasetsEl = $('<div class="phantasus-preloaded"></div>');
  if (navigator.onLine) {
    html.push('<li role="presentation"><a href="#' + preloaded + '"' +
      ' aria-controls="' + preloaded + '" role="tab" data-toggle="tab"><i class="fa fa-database"></i>' +
      ' Preloaded Datasets</a></li>');

    // lazy load
    new phantasus.SampleDatasets({
      $el: $sampleDatasetsEl,
      show: true,
      callback: function (heatMapOptions) {
        options.optionsCallback(heatMapOptions);
      }
    });
  }

  html.push('</ul>');

  html.push('<div class="tab-content"' +
    ' style="text-align:center;cursor:pointer;height:300px;">');

  html.push('<div role="tabpanel" class="tab-pane active" id="' + myComputer + '">');
  html.push('<div data-name="drop" class="phantasus-file-drop phantasus-landing-panel">');
  html.push('<button class="btn btn-default"><span class="fa-stack"><i' +
    ' class="fa fa-file-o' +
    ' fa-stack-2x"></i> <i class="fa fa-plus fa-stack-1x"></i></span> Select File</button>' +
    ' <div style="padding-top:10px;">or Copy and Paste Clipboard Data, <span' +
    ' class="phantasus-drag-text">Drag and' +
    ' Drop</span></div>');
  html.push('<input name="hiddenFile" style="display:none;" type="file">');
  html.push('</div>');
  html.push('</div>');

  html.push('<div role="tabpanel" class="tab-pane" id="' + url + '">');
  html.push('<div class="phantasus-landing-panel">');
  html.push('<input name="url" placeholder="Enter a URL" class="form-control"' +
    ' style="display:inline;max-width:400px;' +
    ' type="text"><button name="openUrl" class="btn btn-default"' +
    ' type="button">Go</button>');
  html.push('</div>');
  html.push('</div>');

  if (typeof Dropbox !== 'undefined') {
    html.push('<div role="tabpanel" class="tab-pane" id="' + dropbox + '">');
    html.push('<div class="phantasus-landing-panel">');
    html.push('<button name="dropbox" class="btn btn-default">Browse Dropbox</button>');
    html.push('</div>');
    html.push('</div>');
  }
  if (typeof gapi !== 'undefined') {
    html.push('<div role="tabpanel" class="tab-pane" id="' + googleId + '">');
    html.push('<div class="phantasus-landing-panel">');
    html.push('<button name="google" class="btn btn-default">Browse Google Drive</button>');
    html.push('</div>');
    html.push('</div>');
  }
  if (navigator.onLine) {
    html.push('<div role="tabpanel" class="tab-pane" id="' + preloaded + '">');
    html.push('<div class="phantasus-landing-panel">');
    html.push('</div>');
    html.push('</div>');
  }
  html.push('</div>'); // tab-content
  html.push('</div>');
  var $el = $(html.join(''));
  $sampleDatasetsEl.appendTo($el.find('#' + preloaded + ' > .phantasus-landing-panel'));
  this.$el = $el;

  var $file = $el.find('[name=hiddenFile]');
  var $myComputer = $el.find('[id=' + myComputer + ']');
  this.$el.find('.nav').on('click', 'li > a', function (e) {
    e.preventDefault();
    $(this).tab('show');
  });

  var $url = $el.find('[name=url]');
  $url.on('keyup', function (evt) {
    if (evt.which === 13) {
      var text = $.trim($(this).val());
      if (text !== '') {
        options.fileCallback(text);
      }
    }
  });
  $el.find('[name=openUrl]').on('click', function (evt) {
    var text = $.trim($url.val());
    if (text !== '') {
      options.fileCallback(text);
    }
  });

  var $dropbox = $el.find('[name=dropbox]');
  $dropbox.on('click', function (e) {
    Dropbox.choose({
      success: function (results) {
        var val = results[0].link;
        options.fileCallback(val);
      },
      linkType: 'direct',
      multiselect: false

    });
  });

  var $google = $el.find('[name=google]');
  $google.on('click', function () {
    var developerKey = 'AIzaSyBCRqn5xgdUsJZcC6oJnIInQubaaL3aYvI';
    var clientId = '936482190815-85k6k06b98ihv272n0b7f7fm33v5mmfa.apps.googleusercontent.com';
    var scope = ['https://www.googleapis.com/auth/drive'];
    var oauthToken;
    var pickerApiLoaded = false;
    var oauthToken;

    // Use the API Loader script to load google.picker and gapi.auth.
    function onApiLoad() {
      gapi.load('auth', {'callback': onAuthApiLoad});
      gapi.load('picker', {'callback': onPickerApiLoad});
    }

    function onAuthApiLoad() {
      window.gapi.auth.authorize(
        {
          'client_id': clientId,
          'scope': scope,
          'immediate': false
        },
        handleAuthResult);
    }

    function onPickerApiLoad() {
      pickerApiLoaded = true;
      createPicker();
    }

    function handleAuthResult(authResult) {
      if (authResult && !authResult.error) {
        oauthToken = authResult.access_token;
        createPicker();
      }
    }

    // Create and render a Picker object for picking user Photos.
    function createPicker() {
      if (pickerApiLoaded && oauthToken) {
        var picker = new google.picker.PickerBuilder().addView(google.picker.ViewId.DOCS)
          .setOAuthToken(oauthToken)
          .setDeveloperKey(developerKey)
          .setCallback(pickerCallback)
          .build();
        picker.setVisible(true);
        $('.picker-dialog-bg').css('z-index', 1052); // make it appear above modals
        $('.picker-dialog').css('z-index', 1053);
      }
    }

    function pickerCallback(data) {
      if (data.action == google.picker.Action.PICKED) {
        var file = data.docs[0];
        var fileName = file.name;
        var accessToken = gapi.auth.getToken().access_token;
        var xhr = new XMLHttpRequest();
        var url = new String('https://www.googleapis.com/drive/v3/files/' + file.id + '?alt=media');
        url.name = fileName;
        url.headers = {'Authorization': 'Bearer ' + accessToken};
        options.fileCallback(url);
      }

    }

    onApiLoad();
  });
  $file.on('change', function (evt) {
    var files = evt.target.files; // FileList object
    for (var i = 0; i < files.length; i++) {
      options.fileCallback(files[i]);
    }
  });

  $(window).on('paste.phantasus', this.paste = function (e) {
    if ($myComputer.is(':visible')) {
      var text = e.originalEvent.clipboardData.getData('text/plain');
      if (text != null && text.length > 0) {
        e.preventDefault();
        e.stopPropagation();
        var url;
        if (text.indexOf('http') === 0) {
          url = text;
        } else {
          var blob = new Blob([text]);
          url = new String(window.URL.createObjectURL(blob));
          url.name = 'clipboard';
        }
        options.fileCallback(url);
      }
    }
  });
  var $drop = $el.find('[data-name=drop]');
  var _this = this;
  $el.on('remove', function () {
    $(window).off(_this.paste).off(_this.dragover).off(_this.dragenter).off(_this.dragleave).off(_this.drop);
  });
  var clicking = false;
  $drop.on('click', function (e) {
    if (!clicking) {
      clicking = true;
      $file.click();
      clicking = false;
    }
    // e.preventDefault();
  });
  $(window).on(
    'dragover',
    this.dragover = function (e) {
      if ($myComputer.is(':visible')) {
        $drop.addClass('drag');
        e.preventDefault();
        e.stopPropagation();
      }
    }).on(
    'dragenter',
    this.dragenter = function (e) {
      if ($myComputer.is(':visible')) {
        $drop.addClass('drag');
        e.preventDefault();
        e.stopPropagation();
      }
    }).on('dragleave', this.dragleave = function (e) {
    if ($myComputer.is(':visible')) {
      $drop.removeClass('drag');
      e.preventDefault();
      e.stopPropagation();
    }
  }).on('drop', this.drop = function (e) {
    if ($myComputer.is(':visible')) {
      $drop.removeClass('drag');
      if (e.originalEvent.dataTransfer) {
        if (e.originalEvent.dataTransfer.files.length) {
          e.preventDefault();
          e.stopPropagation();
          var files = e.originalEvent.dataTransfer.files;
          for (var i = 0; i < files.length; i++) {
            options.fileCallback(files[i]);
          }
        } else {
          var url = e.originalEvent.dataTransfer.getData('URL');
          options.fileCallback(url);
          e.preventDefault();
          e.stopPropagation();
        }
      }
    }
  });
};

phantasus.FilterUI = function (project, isColumns) {
  var _this = this;
  this.project = project;
  this.isColumns = isColumns;
  var $div = $('<div style="min-width:180px;"></div>');
  this.$div = $div;
  $div.append(this.addBase());
  var $filterMode = $div.find('[name=filterMode]');
  $filterMode.on('change', function (e) {
    var isAndFilter = $filterMode.prop('checked');
    (isColumns ? project.getColumnFilter() : project.getRowFilter())
      .setAnd(isAndFilter);
    isColumns ? _this.project.setColumnFilter(_this.project
      .getColumnFilter(), true) : _this.project.setRowFilter(
      _this.project.getRowFilter(), true);
    e.preventDefault();
  });

  $div.on('click', '[data-name=add]', function (e) {
    var $this = $(this);
    var $row = $this.closest('.phantasus-entry');
    // add after
    var index = $row.index();
    var newFilter = new phantasus.AlwaysTrueFilter();
    (isColumns ? project.getColumnFilter() : project.getRowFilter())
      .insert(index, newFilter);
    $row.after(_this.add(newFilter));
    e.preventDefault();
  });
  $div.on('click', '[data-name=delete]', function (e) {
    var $this = $(this);
    var $row = $this.closest('.phantasus-entry');
    var index = $row.index() - 1;
    (isColumns ? project.getColumnFilter() : project.getRowFilter())
      .remove(index);
    $row.remove();
    isColumns ? _this.project.setColumnFilter(_this.project
    .getColumnFilter(), true) : _this.project.setRowFilter(
      _this.project.getRowFilter(), true);
    e.preventDefault();
  });
  $div.on('submit', 'form', function (e) {
    var $this = $(this);
    e.preventDefault();
  });
  $div.on('change', '[name=by]', function (e) {
    var $this = $(this);
    var fieldName = $this.val();
    var $row = $this.closest('.phantasus-entry');
    var index = $row.index() - 1;
    if (fieldName == '') {
      $row.find('[data-name=ui]').empty();
    } else {
      _this.createFilter({
        fieldName: fieldName,
        $div: $this
      });
    }

    isColumns ? _this.project.setColumnFilter(_this.project
      .getColumnFilter(), true) : _this.project.setRowFilter(
      _this.project.getRowFilter(), true);
  });
  // show initial filters
  var combinedFilter = (isColumns ? project.getColumnFilter() : project
  .getRowFilter());
  var filters = combinedFilter.getFilters ? combinedFilter.getFilters() : [];
  for (var i = 0; i < filters.length; i++) {
    this.createFilter({
      filter: filters[i]
    });
  }
  if (combinedFilter.on) {
    combinedFilter.on('add', function (e) {
      _this.createFilter({
        filter: e.filter
      });
    });
    combinedFilter.on('remove', function (e) {
      // offset of 1 for header
      var $row = $div.find('.phantasus-entry')[1 + e.index].remove();
    });
    combinedFilter.on('and', function (e) {
      $filterMode.prop('checked', e.source.isAnd());
    });

  }
};

phantasus.FilterUI.rangeFilter = function (project, name, isColumns, $ui, filter) {
  $ui.empty();
  var html = [];
  html.push('<label>Range of values</label><br />');
  html
    .push('<label>>= </label> <input style="max-width:200px;" class="form-control input-sm" name="min" type="text" />');
  html
    .push('<label> and <= </label> <input style="max-width:200px;" class="form-control input-sm" name="max" type="text" />');
  html.push('<br /><a data-name="switch" href="#">Switch to top filter</a>');
  var $form = $(html.join(''));
  $form.appendTo($ui);
  $ui.find('[data-name=switch]')
    .on(
      'click',
      function (e) {
        e.preventDefault();
        var newFilter = phantasus.FilterUI.topFilter(project,
          name, isColumns, $ui);
        var index = -1;
        var filters = isColumns ? project.getColumnFilter()
          .getFilters() : project.getRowFilter()
          .getFilters();
        for (var i = 0; i < filters.length; i++) {
          if (filters[i] === filter) {
            index = i;
            break;
          }
        }
        if (index === -1) {
          throw new Error('Filter not found.');
        }
        (isColumns ? project.getColumnFilter() : project
          .getRowFilter()).set(index, newFilter);
        isColumns ? project.setColumnFilter(project
          .getColumnFilter(), true) : project
          .setRowFilter(project.getRowFilter(), true);
      });
  var $min = $ui.find('[name=min]');
  var $max = $ui.find('[name=max]');
  if (!filter) {
    filter = new phantasus.RangeFilter(-Number.MAX_VALUE, Number.MAX_VALUE,
      name, isColumns);
  } else {
    $min.val(filter.min);
    $max.val(filter.max);
  }

  $min.on('keyup', _.debounce(function (e) {
    filter.setMin(parseFloat($.trim($(this).val())));
    isColumns ? project.setColumnFilter(project.getColumnFilter(), true)
      : project.setRowFilter(project.getRowFilter(), true);

  }, 500));
  $max.on('keyup', _.debounce(function (e) {
    filter.setMax(parseFloat($.trim($(this).val())));
    isColumns ? project.setColumnFilter(project.getColumnFilter(), true)
      : project.setRowFilter(project.getRowFilter(), true);

  }, 500));

  return filter;

};
phantasus.FilterUI.topFilter = function (project, name, isColumns, $ui, filter) {
  $ui.empty();
  var html = [];
  html.push('<label>Top</label><br />');
  html
    .push('<select style="width:auto;" class="form-control input-sm" name="direction"><option value="Top">Top</option><option value="Bottom">Bottom</option><option value="TopBottom">Top/Bottom</option></select>');
  html
    .push(' <label>N </label> <input style="max-width:200px;" class="form-control input-sm" name="n" type="text" />');
  html.push('<br /><a data-name="switch" href="#">Switch to range filter</a>');
  var $form = $(html.join(''));
  $form.appendTo($ui);
  var $n = $ui.find('[name=n]');
  var $direction = $ui.find('[name=direction]');
  $ui.find('[data-name=switch]')
    .on(
      'click',
      function (e) {
        e.preventDefault();
        var newFilter = phantasus.FilterUI.rangeFilter(project,
          name, isColumns, $ui);
        var index = -1;
        var filters = isColumns ? project.getColumnFilter()
          .getFilters() : project.getRowFilter()
          .getFilters();
        for (var i = 0; i < filters.length; i++) {
          if (filters[i] === filter) {
            index = i;
            break;
          }
        }
        if (index === -1) {
          throw new Error('Filter not found.');
        }
        (isColumns ? project.getColumnFilter() : project
          .getRowFilter()).set(index, newFilter);
        isColumns ? project.setColumnFilter(project
          .getColumnFilter(), true) : project
          .setRowFilter(project.getRowFilter(), true);
      });
  if (!filter) {
    filter = new phantasus.TopNFilter(NaN, phantasus.TopNFilter.TOP, name, isColumns);
  } else {
    var dirVal;
    if (filter.direction === phantasus.TopNFilter.TOP) {
      dirVal = 'Top';
    } else if (filter.direction === phantasus.TopNFilter.BOTTOM) {
      dirVal = 'Bottom';
    } else {
      dirVal = 'TopBottom';
    }
    $direction.val(dirVal);
    $n.val(filter.n);
  }

  $direction.on('change', function () {
    var dir = $(this).val();
    var dirVal;
    if (dir === 'Top') {
      dirVal = phantasus.TopNFilter.TOP;
    } else if (dir === 'Bottom') {
      dirVal = phantasus.TopNFilter.BOTTOM;
    } else {
      dirVal = phantasus.TopNFilter.TOP_BOTTOM;
    }
    filter.setDirection(dirVal);

    isColumns ? project.setColumnFilter(project.getColumnFilter(), true)
      : project.setRowFilter(project.getRowFilter(), true);
  });
  $n.on('keyup', _.debounce(function (e) {
    filter.setN(parseInt($.trim($(this).val())));
    isColumns ? project.setColumnFilter(project.getColumnFilter(), true)
      : project.setRowFilter(project.getRowFilter(), true);

  }, 500));

  return filter;
};
phantasus.FilterUI.prototype = {
  /**
   *
   * @param options
   *            options.$div div to add filter to or null to add to end
   *            options.filter Pre-existing filter or null to create filter
   *            options.fieldName Field name to filter on
   */
  createFilter: function (options) {
    var index = -1;
    var $div = options.$div;
    var isColumns = this.isColumns;
    var filter = options.filter;
    var project = this.project;
    var fieldName = filter ? filter.name : options.fieldName;
    var $ui;
    if (!$div) {
      // add filter to end
      var $add = $(this.add(filter));
      $add.appendTo(this.$div);
      $ui = $add.find('[data-name=ui]');
    } else { // existing $div
      var $row = $div.closest('.phantasus-entry');
      index = $row.index() - 1;
      $ui = $row.find('[data-name=ui]');
    }

    $ui.empty();
    var vector = (isColumns ? this.project.getFullDataset()
    .getColumnMetadata() : this.project.getFullDataset()
    .getRowMetadata()).getByName(fieldName);

    if (filter instanceof phantasus.RangeFilter) {
      phantasus.FilterUI.rangeFilter(project, fieldName, isColumns, $ui,
        filter);
    } else if (filter instanceof phantasus.TopNFilter) {
      phantasus.FilterUI.topFilter(project, fieldName, isColumns, $ui,
        filter);
    } else if (filter == null && phantasus.VectorUtil.isNumber(vector)
      && phantasus.VectorUtil.containsMoreThanNValues(vector, 9)) {
      filter = phantasus.FilterUI.rangeFilter(project, fieldName,
        isColumns, $ui, filter);
    } else {
      var set = phantasus.VectorUtil.getSet(vector);
      var array = set.values();
      array.sort(phantasus.SortKey.ASCENDING_COMPARATOR);
      if (!filter) {
        filter = new phantasus.VectorFilter(new phantasus.Set(), set
          .size(), fieldName, isColumns);
      } else {
        filter.maxSetSize = array.length;
      }

      var checkBoxList = new phantasus.CheckBoxList({
        responsive: false,
        $el: $ui,
        items: array,
        set: filter.set
      });
      checkBoxList.on('checkBoxSelectionChanged', function () {
        isColumns ? project.setColumnFilter(project.getColumnFilter(),
          true) : project.setRowFilter(project.getRowFilter(),
          true);

      });
    }
    if (index !== -1) {
      // set the filter index
      if (fieldName !== '') {
        (isColumns ? project.getColumnFilter() : project.getRowFilter())
          .set(index, filter);
      } else {
        (isColumns ? project.getColumnFilter() : project.getRowFilter())
          .set(index, new phantasus.AlwaysTrueFilter());
      }
    }
    return filter;
  },

  addBase: function () {
    var html = [];
    html
      .push('<div style="padding-bottom:2px;border-bottom:1px solid #eee" class="phantasus-entry">');
    html.push('<div class="row">');
    html
      .push('<div class="col-xs-12">'
        + '<div class="checkbox"><label><input type="checkbox" name="filterMode">Pass all filters</label></div> '

        + '</div>');
    html.push('</div>');
    html.push('<div class="row">');
    html
      .push('<div class="col-xs-8"><a class="btn btn-default btn-xs" role="button"' +
        ' data-name="add" href="#">Add</a></div>');

    html.push('</div>');
    html.push('</div>');
    return html.join('');
  },
  add: function (filter) {
    var project = this.project;
    var isColumns = this.isColumns;
    var fields = phantasus.MetadataUtil.getMetadataNames(isColumns ? project
      .getFullDataset().getColumnMetadata() : project
      .getFullDataset().getRowMetadata());
    var html = [];
    html.push('<div class="phantasus-entry">');

    html.push('<div class="form-group">');
    html.push('<label>Field</label>');
    // field

    html
      .push('<select style="max-width:160px;overflow-x:hidden;" name="by" class="form-control input-sm">');
    html.push('<option value=""></option>');
    var filterField = filter ? filter.toString() : null;

    _.each(fields, function (field) {
      html.push('<option value="' + field + '"');
      if (field === filterField) {
        html.push(' selected');
      }
      html.push('>');
      html.push(field);
      html.push('</option>');
    });
    html.push('</select>');
    html.push('</div>');
    html.push('<div class="row">');
    // filter ui
    html.push('<div data-name="ui" class="col-xs-12"></div>');
    html.push('</div>');

    // end filter ui

    // add/delete
    html
      .push('<div style="padding-bottom:6px; border-bottom:1px solid #eee" class="row">');

    html.push('<div class="col-xs-11">');

    html
      .push('<a class="btn btn-default btn-xs" role="button" data-name="delete"' +
        ' href="#">Remove</a>');
    html.push('</div>');

    html.push('</div>'); // row
    html.push('</div>'); // phantasus-entry
    return html.join('');
  }
};

/**
 *
 * @param options.fontModel
 * @param options.track
 * @param options.heatMap
 * @constructor
 */
phantasus.FontChooser = function (options) {
  var _this = this;
  var fontModel = options.fontModel;
  var track = options.track;
  var heatMap = options.heatMap;
  // ensure map exists
  fontModel.getMappedValue(track.getVector(track.settings.fontField), track.getVector(track.settings.fontField).getValue(0));
  var formBuilder = new phantasus.FormBuilder();
  formBuilder.append({
    value: track.settings.fontField != null,
    type: 'checkbox',
    name: 'use_another_annotation_to_determine_font'
  });
  var annotationNames = phantasus.MetadataUtil.getMetadataNames(
    track.isColumns ? heatMap.getProject().getFullDataset().getColumnMetadata() : heatMap.getProject().getFullDataset().getRowMetadata());
  annotationNames.splice(annotationNames.indexOf(track.getName()), 1);
  formBuilder.append({
    name: 'annotation_name',
    type: 'bootstrap-select',
    options: annotationNames,
    search: annotationNames.length > 10,
    value: track.settings.fontField
  });
  formBuilder.setVisible('annotation_name', track.settings.fontField != null);
  formBuilder.append({
    name: 'selected_value',
    type: 'bootstrap-select',
    search: true,
    options: fontModel.getMap(track.settings.fontField != null ? track.settings.fontField : track.getName()).keys()
  });
  var $selectedValue = formBuilder.find('selected_value');
  formBuilder.append({
    name: 'selected_font',
    type: 'bootstrap-select',
    options: [{name: 'normal', value: 400}, {name: 'bold', value: 700}, {name: 'bolder', value: 900}]
  });

  var repaint = function () {
    track.setInvalid(true);
    track.repaint();
  };
  formBuilder.find('use_another_annotation_to_determine_font').on('change', function () {
    var checked = $(this).prop('checked');
    formBuilder.setValue('annotation_name', null);
    formBuilder.setValue('selected_value', null);
    formBuilder.setVisible('annotation_name', checked);
    if (!checked) {
      track.settings.fontField = null;
    }
    repaint();
  });
  formBuilder.find('annotation_name').on('change', function () {
    var annotationName = $(this).val();
    fontModel.getMappedValue(track.getVector(annotationName), track.getVector(annotationName).getValue(0));
    track.settings.fontField = annotationName;
    // ensure map exists
    formBuilder.setOptions('selected_value', fontModel.getMap(track.settings.fontField != null ? track.settings.fontField : track.getName()).keys());
    formBuilder.setValue('selected_value', null);
    repaint();
  });

  var $selectedFont = formBuilder.find('selected_font');
  $selectedFont.on('change', function (e) {
    fontModel.setMappedValue(track.getVector(track.settings.fontField), $selectedValue.val(), {weight: $(this).val()});
    repaint();
  });

  var updateMappedValue = function () {
    var selectedVal = $selectedValue.val();
    var mappedValue = fontModel.getMappedValue(track.getVector(track.settings.fontField), selectedVal);
    formBuilder.setValue('selected_font', mappedValue.weight);
  };
  $selectedValue.on('change', function () {
    // update displayed value
    updateMappedValue();
  });
  updateMappedValue();
  this.$div = formBuilder.$form;

};
phantasus.Util.extend(phantasus.FontChooser, phantasus.Events);

phantasus.FormBuilder = function (options) {
  var _this = this;
  this.prefix = _.uniqueId('form');
  this.$form = $('<form></form>');
  this.$form.attr('role', 'form').attr('id', this.prefix);
  this.formStyle = options == null || options.formStyle == null ? 'horizontal' : options.formStyle;
  this.$form.addClass('phantasus');
  if (this.formStyle === 'horizontal') {
    this.titleClass = 'col-xs-12 control-label';
    this.labelClass = 'col-xs-4 control-label';
    this.$form.addClass('form-horizontal');
  } else if (this.formStyle === 'vertical') {
    this.labelClass = 'control-label';
    this.titleClass = 'control-label';
  } else if (this.formStyle === 'inline') {
    this.titleClass = '';
    this.labelClass = '';
    this.$form.addClass('form-inline');
  }
  this.$form.on('submit', function (e) {
    e.preventDefault();
  });
  this.$form.on(
    'dragover',
    function (e) {
      var node = $(e.originalEvent.srcElement).parent().parent()
        .prev();
      if (node.is('select') && node.hasClass('file-input')) {
        $(e.originalEvent.srcElement).parent().css('border',
          '1px solid black');
        e.preventDefault();
        e.stopPropagation();
      }
    }).on(
    'dragenter',
    function (e) {
      var node = $(e.originalEvent.srcElement).parent().parent()
        .prev();
      if (node.is('select') && node.hasClass('file-input')) {
        $(e.originalEvent.srcElement).parent().css('border',
          '1px solid black');
        e.preventDefault();
        e.stopPropagation();
      }
    }).on('dragleave', function (e) {
    var node = $(e.originalEvent.srcElement).parent().parent().prev();
    if (node.is('select') && node.hasClass('file-input')) {
      $(e.originalEvent.srcElement).parent().css('border', '');
      e.preventDefault();
      e.stopPropagation();
    }
  }).on('drop', function (e) {
    var node = $(e.originalEvent.srcElement).parent().parent().prev();
    if (node.is('select') && node.hasClass('file-input')) {
      var isMultiple = node.data('multiple'); // multiple files?
      $(e.originalEvent.srcElement).parent().css('border', '');
      var name = node.attr('name');
      name = name.substring(0, name.length - '_picker'.length);
      if (e.originalEvent.dataTransfer) {
        if (e.originalEvent.dataTransfer.files.length) {
          e.preventDefault();
          e.stopPropagation();
          var files = e.originalEvent.dataTransfer.files;
          _this.setValue(name, isMultiple ? files : files[0]);
          _this.trigger('change', {
            name: name,
            value: files[0]
          });
        } else {
          var url = e.originalEvent.dataTransfer.getData('URL');
          e.preventDefault();
          e.stopPropagation();
          _this.setValue(name, isMultiple ? [url] : url);
          _this.trigger('change', {
            name: name,
            value: url
          });
        }
      }
    }
  });
  // this.labelColumnDef = '4';
  // this.fieldColumnDef = '8';
};

phantasus.FormBuilder.showProgressBar = function (options) {
  var content = [];
  content.push('<div class="container-fluid">');
  content.push('<div class="row">');
  content.push('<div class="col-xs-8">');
  content
    .push(
      '<div class="progress progress-striped active"><div class="progress-bar" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"></div></div>');
  content.push('</div>'); // col
  content.push('<div class="col-xs-2">');
  content
    .push('<input class="btn btn-default" type="button" name="stop" value="Cancel">');
  content.push('</div>'); // col
  content.push('</div>'); // row
  if (options.subtitle) {
    content.push('<div class="row"><div class="col-xs-8">');
    content.push('<p class="text-muted">');
    content.push(options.subtitle);
    content.push('</p>');
    content.push('</div></div>');
  }
  content.push('</div>');
  var $content = $(content.join(''));
  $content.find('[name=stop]').on('click', function (e) {
    options.stop();
    e.preventDefault();
  });
  return phantasus.FormBuilder.showInDraggableDiv({
    title: options.title,
    $content: $content
  });
};
phantasus.FormBuilder.showInDraggableDiv = function (options) {
  var width = options.width || '300px';
  var html = [];
  html
    .push('<div style="z-index: 1050; top: 100px; position:absolute; padding-left:10px; padding-right:10px; width:'
      + width
      + ' ; background:white; box-shadow: 0 5px 15px rgba(0,0,0,0.5); border: 1px solid rgba(0,0,0,0.2); border-radius: 6px;">');

  if (options.title != null) {
    html
      .push('<h4 style="cursor:move; border-bottom: 1px solid #e5e5e5;" name="header">'
        + options.title + '</h4>');
  }
  html.push('<div name="content"></div>');
  html.push('</div>');

  var $div = $(html.join(''));
  var $content = $div.find('[name=content]');
  $div.find('[name=header]').on('dblclick', function () {
    if ($content.css('display') === 'none') {
      $content.css('display', '');
    } else {
      $content.css('display', 'none');
    }
  });

  options.$content.appendTo($content);
  $div.css('left', ($(window).width() / 2) - $content.outerWidth() / 2);
  $div.draggable({
    //handle: '[name=header]',
    containment: 'document'
  });
  // $div.resizable();
  $div.appendTo(options.appendTo != null ? options.appendTo : $(document.body));
  return $div;
};

phantasus.FormBuilder.showMessageModal = function (options) {
  var $div = phantasus.FormBuilder
    ._showInModal({
      modalClass: options.modalClass,
      title: options.title,
      html: options.html,
      footer: ('<button type="button" class="btn btn-default"' +
        ' data-dismiss="modal">OK</button>'),
      backdrop: options.backdrop,
      size: options.size,
      focus: options.focus,
      appendTo: options.appendTo
    });
  $div.find('button').focus();
  return $div;

  // if (options.draggable) {
  // $div.draggable({
  // handle : $div.find(".modal-header")
  // });
  // }
};

phantasus.FormBuilder._showInModal = function (options) {
  var html = [];
  options = $.extend({}, {
    size: '',
    close: true,
    modalClass: ''
  }, options);
  html.push('<div tabindex="-1" class="modal' + (options.modalClass ? (' ' + options.modalClass) : '') + '" role="dialog"' +
    ' aria-hidden="false"');
  if (options.z) {
    html.push(' style="z-index: ' + options.z + ' !important;"');
  }
  html.push('>');
  html.push('<div class="modal-dialog ' + options.size + '">');
  html.push('<div class="modal-content">');
  html.push(' <div class="modal-header">');
  if (options.close) {
    html
      .push('  <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>');
  }
  if (options.title != null) {
    html.push('<h4 class="modal-title">' + options.title + '</h4>');
  }
  html.push('</div>');
  html.push('<div class="modal-body">');
  html.push('</div>');
  if (options.footer) {
    html.push('<div class="modal-footer">');
    html.push(options.footer);
  }
  html.push('</div>');
  html.push('</div>');
  html.push('</div>');
  html.push('</div>');
  var $div = $(html.join(''));
  $div.on('mousewheel', function (e) {
    e.stopPropagation();
  });
  $div.find('.modal-body').html(options.html);
  $div.prependTo(options.appendTo != null ? options.appendTo : $(document.body));
  $div.modal({
    keyboard: true,
    backdrop: options.backdrop === true ? true : false
  }).on('hidden.bs.modal', function (e) {
    $div.remove();
    if (options.onClose) {
      options.onClose();
    }
    if (options.focus) {
      $(options.focus).focus();
    }
  });

  return $div;
};
/**
 *
 * @param options.z Modal z-index
 * @param options.title Modal title
 * @param options.html Model content
 * @param options.close Whether to show a close button in the footer
 * @param options.onClose {Function} Funtion to invoke when modal is hidden
 * @param options.backdrop Whether to show backdrop
 * @param.options Modal size
 * @param options.focus Element to return focus to when modal is hidden
 * @param options.modalClass
 */
phantasus.FormBuilder.showInModal = function (options) {
  return phantasus.FormBuilder
    ._showInModal({
      modalClass: options.modalClass,
      title: options.title,
      html: options.html,
      footer: options.close ? ('<button type="button" class="btn btn-default" data-dismiss="modal">'
        + options.close + '</button>')
        : null,
      onClose: options.onClose,
      appendTo: options.appendTo,
      backdrop: options.backdrop,
      size: options.size,
      focus: options.focus
    });
  // if (options.draggable) {
  // $div.draggable({
  // handle : $div.find(".modal-header")
  // });
  // }
};

/**
 *
 * @param options.ok
 * @param options.cancel
 * @param options.apply
 * @param options.title
 * @param options.content
 * @param options.okCallback
 * @param options.cancelCallba
 * @param options.okFocus
 *
 */
phantasus.FormBuilder.showOkCancel = function (options) {
  options = $.extend({}, {
    ok: true,
    cancel: true
  }, options);
  var footer = [];
  if (options.ok) {
    footer
      .push('<button name="ok" type="button" class="btn btn-default">OK</button>');
  }
  if (options.apply) {
    footer
      .push('<button name="apply" type="button" class="btn btn-default">Apply</button>');
  }
  if (options.cancel) {
    footer
      .push('<button name="cancel" type="button" data-dismiss="modal" class="btn btn-default">Cancel</button>');
  }
  var $div = phantasus.FormBuilder._showInModal({
    title: options.title,
    html: options.content,
    footer: footer.join(''),
    size: options.size,
    close: options.close,
    onClose: options.onClose,
    focus: options.focus,
    appendTo: options.appendTo
  });
  // if (options.align === 'right') {
  // $div.css('left', $(window).width()
  // - $div.find('.modal-content').width() - 60);
  // }

  var $ok = $div.find('[name=ok]');
  $ok.on('click', function (e) {
    if (options.okCallback) {
      options.okCallback();
    }
    $div.modal('hide');
  });
  $div.find('[name=cancel]').on('click', function (e) {
    if (options.cancelCallback) {
      options.cancelCallback();
    }
    $div.modal('hide');
  });
  if (options.okFocus) {
    $ok.focus();
  }

  if (options.draggable) {
    $div.draggable({
      handle: '.modal-header',
      containment: 'document'
    });
  }
  return $div;
};

phantasus.FormBuilder.hasChanged = function (object, keyToUIElement) {
  var keys = _.keys(keyToUIElement);
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var value = object[key];
    var $element = keyToUIElement[key];
    if (value !== phantasus.FormBuilder.getValue($element)) {
      return true;
    }
  }
  return false;
};
phantasus.FormBuilder.getValue = function ($element) {
  var list = $element.data('phantasus.checkbox-list');
  if (list != null) {
    return list.val();
  }
  if ($element.attr('type') === 'radio') {
    return $element.filter(':checked').val();
  }
  if ($element.data('type') === 'file') {
    return $element.data('files');
  }
  return $element.attr('type') === 'checkbox' ? $element.prop('checked') : $element.val();
};

phantasus.FormBuilder.prototype = {
  appendContent: function ($content) {
    this.$form.append($content);
  },
  addSeparator: function () {
    var html = [];
    html.push('<div class="form-group">');
    if (this.formStyle === 'horizontal') {
      html.push('<div class="col-xs-12">');
    }
    html.push('<hr />');
    if (this.formStyle === 'horizontal') {
      html.push('</div>');
    }
    html.push('</div>');
    this.$form.append(html.join(''));
  },
  _append: function (html, field, isFieldStart) {
    var _this = this;
    var required = field.required;
    var name = field.name;
    var type = field.type;
    if (type == 'separator') {
      if (this.formStyle === 'horizontal') {
        html.push('<div class="col-xs-12">');
      } else {
        html.push('<div class="form-group">');
      }

      html.push('<hr />');
      html.push('</div>');
      return;
    }
    var title = field.title;
    var disabled = field.disabled;
    var help = field.help;
    var value = field.value;
    var showLabel = field.showLabel;
    var tooltipHelp = field.tooltipHelp;
    var style = field.style || '';
    var col = '';
    var labelColumn = '';
    if (this.formStyle === 'horizontal') {
      col = field.col || 'col-xs-8';
    }

    if (showLabel === undefined) {
      showLabel = 'checkbox' !== type && 'button' !== type
        && 'radio' !== type;
      showLabel = showLabel || field.options !== undefined;
    }
    var id = _this.prefix + '_' + name;
    if (title === undefined) {
      title = name.replace(/_/g, ' ');
      title = title[0].toUpperCase() + title.substring(1);
    }
    var endingDiv = false;
    if (showLabel) {
      html.push('<label for="' + id + '" class="' + this.labelClass
        + '">');
      html.push(title);
      if (tooltipHelp) {
        html.push('<div style="padding-left: 5px;" class="fa fa-question-circle" data-toggle="tooltip" data-placement="top" title="' + tooltipHelp + '"></div>');

      }
      html.push('</label>');
      if (isFieldStart && this.formStyle !== 'inline') {
        html.push('<div class="' + col + '">');
        endingDiv = true;
      }
    } else if (isFieldStart && this.formStyle === 'horizontal') { // no label
      html.push('<div class="col-xs-offset-4 ' + col + '">');
      endingDiv = true;
    }
    if ('radio' === type) {
      if (field.options) {
        _.each(field.options,
          function (choice) {
            var isChoiceObject = _.isObject(choice)
              && choice.value !== undefined;
            var optionValue = isChoiceObject ? choice.value
              : choice;
            var optionText = isChoiceObject ? choice.name
              : choice;
            var selected = value === optionValue;
            html.push('<div class="radio"><label>');
            html.push('<input style="' + style + '" value="' + optionValue
              + '" name="' + field.name
              + '" type="radio"');
            if (selected) {
              html.push(' checked');
            }
            html.push('> ');
            if (choice.icon) {
              html.push('<span class="' + choice.icon
                + '"></span> ');
            }
            optionText = optionText[0].toUpperCase()
              + optionText.substring(1);
            html.push(optionText);
            html.push('</label></div>');
          });
      } else {
        html.push('<div class="radio"><label>');
        html.push('<input style="' + style + '" value="' + value + '" name="' + name
          + '" id="' + id + '" type="radio"');
        if (field.checked) {
          html.push(' checked');
        }
        html.push('> ');
        html.push(value[0].toUpperCase() + value.substring(1));
        html.push('</label></div>');
      }
    } else if ('checkbox' === type) {
      html.push('<div class="checkbox"><label>');
      html.push('<input style="' + style + '" name="' + name + '" id="' + id
        + '" type="checkbox"');
      if (value) {
        html.push(' checked');
      }
      if (disabled) {
        html.push(' disabled');
      }
      html.push('> ');
      html.push(title);
      html.push('</label></div>');
    } else if ('checkbox-list' === type) {
      html.push('<div name="' + name + '" class="checkbox-list"><div>');
    } else if ('select' == type || type == 'bootstrap-select') {
      // if (field.multiple) {
      // field.type = 'bootstrap-select';
      // type = 'bootstrap-select';
      // }
      if (type == 'bootstrap-select') {
        html.push('<select style="' + style + '" data-live-search="' + (field.search ? true : false) + '" data-selected-text-format="count" name="'
          + name + '" id="' + id
          + '" data-actions-box="' + (field.selectAll ? true : false) + '" class="selectpicker' + (this.formStyle !== 'inline' ? ' form-control' : '') + '"');
      } else {
        html.push('<select style="' + style + '" name="' + name + '" id="' + id
          + '" class="form-control"');
      }
      if (disabled) {
        html.push(' disabled');
      }
      if (field.multiple) {
        html.push(' multiple');
      }
      html.push('>');
      _.each(field.options, function (choice) {
        if (choice && choice.divider) {
          html.push('<option data-divider="true"></option>');
        } else {
          html.push('<option value="');
          var isChoiceObject = _.isObject(choice)
            && choice.value !== undefined;
          var optionValue = isChoiceObject ? choice.value : choice;
          var optionText = isChoiceObject ? choice.name : choice;
          html.push(optionValue);
          html.push('"');
          var selected = false;
          if (_.isObject(value)) {
            selected = value[optionValue];
          } else if (_.isArray(value)) {
            selected = value.indexOf(optionValue) !== -1;
          } else {
            selected = value == optionValue;
          }
          if (selected) {
            html.push(' selected');
          }
          html.push('>');
          html.push(optionText);
          html.push('</option>');
        }
      });
      html.push('</select>');
      if (field.type == 'bootstrap-select' && field.toggle) {
        html.push('<p class="help-block"><a data-name="' + name
          + '_all" href="#">All</a>&nbsp;|&nbsp;<a data-name="' + name
          + '_none" href="#">None</a></p>');
        _this.$form.on('click', '[data-name=' + name + '_all]',
          function (evt) {
            evt.preventDefault();
            var $select = _this.$form
              .find('[name=' + name + ']');
            $select.selectpicker('val', $.map($select
              .find('option'), function (o) {
              return $(o).val();
            }));
            $select.trigger('change');
          });
        _this.$form.on('click', '[data-name=' + name + '_none]',
          function (evt) {
            evt.preventDefault();
            var $select = _this.$form
              .find('[name=' + name + ']');
            $select.selectpicker('val', []);
            $select.trigger('change');
          });
      }
    } else if ('textarea' == type) {
      html.push('<textarea style="' + style + '" id="' + id + '" class="form-control" name="'
        + name + '"');
      if (required) {
        html.push(' required');
      }
      if (field.placeholder) {
        html.push(' placeholder="' + field.placeholder + '"');
      }
      if (disabled) {
        html.push(' disabled');
      }
      html.push('>');
      if (value != null) {
        html.push(value);
      }
      html.push('</textarea>');
    } else if ('button' == type) {
      html.push('<button style="' + style + '" id="' + id + '" name="' + name
        + '" type="button" class="btn btn-default btn-sm">');
      if (field.icon) {
        html.push('<span class="' + field.icon + '"></span> ');
      }
      html.push(value ? value : title);
      html.push('</button>');
    } else if ('custom' === type) {
      html.push(value);
    } else if ('file' === type) {
      var isMultiple = field.multiple == null ? false : field.multiple;
      html
        .push('<select data-multiple="'
          + isMultiple
          + '" data-type="file" title="'
          + (field.placeholder || (isMultiple ? 'Choose one or more files...'
            : 'Choose a file...'))
          + '" name="'
          + name
          + '_picker" data-width="35%" class="file-input selectpicker form-control">');
      var options = [];

      if (field.options) {
        options = options.concat(field.options);

      }


      // data types are file, dropbox, url, GEO, preloaded and predefined
      options.push('My Computer');
      options.push('URL');
      options.push('GEO Datasets');
      if (typeof Dropbox !== 'undefined') {
        options.push('Dropbox');
      }
      options.push('Saved on server datasets');
      if (field.text != null) {
        options.push(field.text);
      }
      _.each(options, function (choice, index) {
        var isChoiceObject = _.isObject(choice)
          && choice.value !== undefined;
        var optionValue = isChoiceObject ? choice.value : choice;
        var optionText = isChoiceObject ? choice.name : choice;
        html.push('<option value="');
        html.push(optionValue);
        html.push('"');
        if (isChoiceObject && choice.disabled) {
          html.push(' disabled');
        }
        if (optionValue === 'Dropbox') {
          html.push(' data-icon="fa fa-dropbox"');
        } else if (optionValue === 'My Computer') {
          html.push(' data-icon="fa fa-desktop"');
        } else if (optionValue === 'URL') {
          html.push(' data-icon="fa fa-external-link"');
        } else if (optionValue === 'GEO Datasets') {
          html.push(' data-icon="fa fa-external-link"');
        } else if (optionValue === 'Saved on server datasets') {
          html.push(' data-icon="fa fa-desktop"');
        }
        html.push('>');
        html.push(optionText);
        html.push('</option>');
      });
      html.push('</select>');

      html.push('<div>');

      html.push('<div id="'+name+'_url"style="display: none">');
      html
        .push('<input placeholder="'
          + (isMultiple ? 'Enter one or more URLs'
            : 'Enter a URL')
          + '" class="form-control" style="width:50%; display:inline-block;" type="text" name="'
          + name + '_url">');
      html.push('<input type="submit" style="margin-left: 10px;" class="btn button-default" value="Load">');
      html.push('</div>');

/*      if (field.preloadedExists) {
        html
          .push('<input placeholder="'
            + 'Enter a name of preloaded dataset this server provides them'
            + '" class="form-control" style="width:50%; display:none;" type="text" name="'
            + name + '_pre">');
      }*/

      if (field.gse !== false) {
        html.push('<div id="'+name+'_geo" style="display: none">');
        html
          .push('<input placeholder="'
            + "Enter a GSE or GDS identifier (e.g. GSE53986)"
            + '" class="form-control" style="width:50%; display:inline-block;" type="text" name="'
            + name + '_geo">');
        html.push('<input type="submit" style="margin-left: 10px;" class="btn button-default" value="Load">');
        html.push('</div>');
      }
      if (field.text) {
        html
          .push('<input class="form-control" style="width:50%; display:none;" type="text" name="'
            + name + '_text">');
      }

      html.push('<div id="'+name+'_pre" style="display: none">');
      html
        .push('<input placeholder="'
          + 'Enter a dataset name here'
          + '" class="form-control" style="width:50%; display:inline-block;" type="text" name="'
          + name + '_pre">');
      html.push('<input type="submit" style="margin-left: 10px;" class="btn button-default" value="Load">');
      html.push('</div>');

      html.push('</div>');

      html.push('<input style="display:none;" type="file" name="' + name
        + '_file"' + (isMultiple ? ' multiple' : '') + '>');
      // browse button clicked
      // select change
      _this.$form
        .on(
          'change',
          '[name=' + name + '_picker]',
          function (evt) {
            var $this = $(this);
            var val = $this.val();
            var showUrlInput = val === 'URL';
            var showGSEInput = val === 'GEO Datasets';
            var showTextInput = val === field.text;
            var showPreInput = val === 'Saved on server datasets';

            if ('Dropbox' === val) {
              var options = {
                success: function (results) {
                  var val = !isMultiple ? results[0].link
                    : results.map(function (result) {
                    return result.link;
                  });
                _this.setValue(name, val);
                _this.trigger('change', {
                  name: name,
                  value: val
                });
              },
              linkType: 'direct',
              multiselect: isMultiple
            };
            Dropbox.choose(options);
            _this.$form.find('[name=' + name + '_picker]').selectpicker('val', '');
          } else if ('My Computer' === val) {
            _this.$form.find('[name=' + name + '_file]')
            .click();
            _this.$form.find('[name=' + name + '_picker]').selectpicker('val', '');
          }

          _this.$form.find('#' + name + '_url')
          .css('display',
            showUrlInput ? '' : 'none');
          _this.$form.find('[name=' + name + '_text]')
          .css('display',
            showTextInput ? '' : 'none');
          _this.$form.find('#' + name + '_geo')
              .css('display',
                showGSEInput ? '' : 'none');
          _this.$form.find('#' + name + '_pre')
              .css('display',
                showPreInput ? '' : 'none');
        });

      // URL
      var URL_dispatcher = function (form) {
        var text = $.trim(form.find('[name=' + name + '_url]').val());;
        if (isMultiple) {
          text = text.split(',').filter(function (t) {
            t = $.trim(t);
            return t !== '';
          });
        }
        _this.trigger('change', {
          name: name,
          value: text
        });
      };

      //??
      _this.$form.on('keyup', '[name=' + name + '_text]', function (evt) {
        var text = $.trim($(this).val());
        _this.setValue(name, text);
        if (evt.which === 13) {
          _this.trigger('change', {
            name: name,
            value: text
          });
        }
      });
      // GEO
      var geo_dispatcher = function (form) {
        var text = $.trim(form.find('[name=' + name + '_geo]').val());
          // console.log('environment', evt);
          // console.log('object to trigger with result', _this, 'name', name, 'text', text);
        _this.trigger('change', {
          name: name,
          value: {
            name: text.toUpperCase(),
            isGEO: true
          }
        })
      };

      // Preloaded
      var PRE_dispatcher = function (form) {
        var text = $.trim(form.find('[name=' + name + '_pre]').val());
        // console.log('environment', evt);
        //console.log('object to trigger with result', _this, 'name', name, 'text', text);
        _this.trigger('change', {
          name: name,
          value: {
            name: text,
            preloaded: true
          }
        })
      };
      // browse file selected
      _this.$form.on('change', '[name=' + name + '_file]', function (evt) {

        var files = evt.target.files; // FileList object
        _this.setValue(name, isMultiple ? files : files[0]);
        _this.trigger('change', {
          name: name,
          value: isMultiple ? files : files[0]
        });
      });

      //SUBMIT
      _this.$form.on('submit', function () {
        var val = $(this).find('[name=' + name + '_picker]').val(); //many
        var showUrlInput = val === 'URL';
        var showGSEInput = val === 'GEO Datasets';
        var showPreInput = val === 'Saved on server datasets';
        if (showGSEInput) geo_dispatcher($(this));
        if (showUrlInput) URL_dispatcher($(this));
        if (showPreInput) PRE_dispatcher($(this));
      });
    } else {
      type = type == null ? 'text' : type;
      if (type === 'div') {
        html.push('<div name="' + name + '" id="' + id + '"');
      } else {
        html.push('<input style="' + style + '" type="' + type
          + '" class="form-control" name="' + name + '" id="'
          + id + '"');
      }
      if (value != null) {
        html.push(' value="' + value + '"');
      }
      if (field.placeholder) {
        html.push(' placeholder="' + field.placeholder + '"');
      }
      if (field.min != null) {
        html.push(' min="' + field.min + '"');
      }
      if (field.max != null) {
        html.push(' max="' + field.max + '"');
      }
      if (field.step) {
        html.push(' step="' + field.step + '"');
      }
      if (required) {
        html.push(' required');
      }
      if (disabled) {
        html.push(' disabled');
      }
      if (field.autocomplete != null) {
        html.push(' autocomplete="' + field.autocomplete + '"');
      }

      html.push('>');
      if (type === 'div') {
        html.push('</div>');
      }
    }
    if (help !== undefined) {
      html.push('<span data-name="' + name + '_help" class="help-block">');
      html.push(help);
      html.push('</span>');
    }
    return endingDiv;
  },
  append: function (fields) {
    var html = [];
    var _this = this;
    var isArray = phantasus.Util.isArray(fields);
    if (!isArray) {
      fields = [fields];
    }
    html.push('<div class="form-group">');
    var endingDiv = false;
    _.each(fields, function (field, index) {
      endingDiv || _this._append(html, field, index === 0);
    });

    html.push('</div>');
    if (endingDiv) {
      html.push('</div>');
    }
    var $div = $(html.join(''));
    this.$form.append($div);
    var checkBoxLists = $div.find('.checkbox-list');
    if (checkBoxLists.length > 0) {
      var checkBoxIndex = 0;
      _.each(fields, function (field) {
        // needs to already be in dom
        if (field.type === 'checkbox-list') {
          var list = new phantasus.CheckBoxList({
            responsive: false,
            $el: $(checkBoxLists[checkBoxIndex]),
            items: field.options
          });

          $(checkBoxLists[checkBoxIndex]).data(
            'phantasus.checkbox-list', list);
          checkBoxIndex++;
        }
      });
    }
    $div.find('.selectpicker').selectpicker({
      iconBase: 'fa',
      tickIcon: 'fa-check',
      style: 'btn-default btn-sm'
    });
  },
  clear: function () {
    this.$form.empty();
  },
  getValue: function (name) {
    var $v = this.$form.find('[name=' + name + ']');
    if ($v.length === 0) {
      $v = this.$form.find('[name=' + name + '_picker]');
    }
    return phantasus.FormBuilder.getValue($v);
  },
  setOptions: function (name, options, selectFirst) {
    var $select = this.$form.find('[name=' + name + ']');
    var checkBoxList = $select.data('phantasus.checkbox-list');
    if (checkBoxList) {
      checkBoxList.setItems(options);
    } else {
      var html = [];
      var selection = $select.val();
      _.each(options, function (choice) {
        var isChoiceObject = _.isObject(choice)
          && choice.value !== undefined;
        if (choice && choice.divider) {
          html.push('<option data-divider="true"></option>');
        } else {
          html.push('<option value="');
          var optionValue = isChoiceObject ? choice.value : choice;
          var optionText = isChoiceObject ? choice.name : choice;
          html.push(optionValue);
          html.push('"');
          html.push('>');
          html.push(optionText);
          html.push('</option>');
        }
      });
      $select.html(html.join(''));
      $select.val(selection);
      if (selectFirst && $select.val() == null) {
        if ($select[0].options.length > 0) {
          $select.val($select[0].options[0].value);
        }
      }
      if ($select.hasClass('selectpicker')) {
        $select.selectpicker('refresh');
        $select.selectpicker('render');
      }
    }
  },
  find: function (name) {
    return this.$form.find('[name=' + name + ']');
  },
  setHelpText: function (name, value) {
    var v = this.$form.find('[data-name=' + name + '_help]');
    v.html(value);
  },
  setValue: function (name, value) {
    var v = this.$form.find('[name=' + name + ']');
    if (v.length === 0) {
      v = this.$form.find('[name=' + name + '_picker]');
      if (v.data('type') === 'file') {
        v.val(value);
        v.selectpicker('render');
        v.data('files', value);
        return;
      }
    }
    var type = v.attr('type');
    var list = v.data('phantasus.checkbox-list');
    if (list) {
      list.setValue(value);
    } else {
      if (type === 'radio') {
        v.filter('[value=' + value + ']').prop('checked', true);
      } else if (type === 'checkbox') {
        v.prop('checked', value);
      } else {
        v.val(value);
      }
      if (v.hasClass('selectpicker')) {
        v.selectpicker('render');
      }
    }

  },
  setVisible: function (name, visible) {
    var $div = this.$form.find('[name=' + name + ']')
      .parents('.form-group');
    if (visible) {
      $div.show();
    } else {
      $div.hide();
    }
  },
  remove: function (name) {
    var $div = this.$form.find('[name=' + name + ']')
      .parents('.form-group');
    $div.remove();
  },
  setEnabled: function (name, enabled) {
    var $div = this.$form.find('[name=' + name + ']');
    $div.attr('disabled', !enabled);
    if (!enabled) {
      $div.parents('.form-group').find('label').addClass('text-muted');
    } else {
      $div.parents('.form-group').find('label').removeClass('text-muted');
    }
  }
};
phantasus.Util.extend(phantasus.FormBuilder, phantasus.Events);

phantasus.GradientColorSupplier = function () {
  phantasus.AbstractColorSupplier.call(this);
  this._updateScale();
};
phantasus.GradientColorSupplier.prototype = {
  createInstance: function () {
    return new phantasus.GradientColorSupplier();
  },
  getColor: function (row, column, value) {
    if (isNaN(value)) {
      return this.missingColor;
    }
    var min = this.min;
    var max = this.max;
    var colors = this.colors;
    if (value <= min) {
      return colors[0];
    } else if (value >= max) {
      return colors[colors.length - 1];
    }
    var fraction = phantasus.SteppedColorSupplier.linearScale(value, min,
        max, 0, 100) / 100;
    return this.colorScale(fraction);
  },
  setFractions: function (options) {
    phantasus.AbstractColorSupplier.prototype.setFractions.call(this,
      options);
    this._updateScale();
  },
  _updateScale: function () {
    this.colorScale = d3.scale.linear().domain(this.fractions).range(
      this.colors).clamp(true);
  }
};
phantasus.Util.extend(phantasus.GradientColorSupplier,
  phantasus.AbstractColorSupplier);

phantasus.Grid = function (options) {
  this.options = options;
  var _this = this;
  var grid;
  this.items = options.items;
  /**
   * Maps from model index to view index. Note that not all model indices are
   * contained in the map because they might have been filtered from the view.
   */
  this.modelToView = null;
  /** view order in model space */
  this.viewOrder = null;
  function getItemColumnValue(item, column) {
    return column.getter(item);
  }

  this.filter = new phantasus.CombinedGridFilter();
  var model = {
    getLength: function () {
      return _this.viewOrder != null ? _this.viewOrder.length
        : _this.items.length;
    },
    getItem: function (index) {
      return _this.items[_this.viewOrder != null ? _this.viewOrder[index]
        : index];
    }
  };
  this.$el = options.$el;

  var gridOptions = $.extend({}, {
    select: true,
    headerRowHeight: 0,
    showHeaderRow: false,
    multiColumnSort: true,
    multiSelect: false,
    topPanelHeight: 0,
    enableColumnReorder: false,
    enableTextSelectionOnCells: true,
    forceFitColumns: true,
    dataItemColumnValueExtractor: getItemColumnValue,
    defaultFormatter: function (row, cell, value, columnDef, dataContext) {
      if (_.isNumber(value)) {
        return phantasus.Util.nf(value);
      } else if (phantasus.Util.isArray(value)) {
        var s = [];
        for (var i = 0, length = value.length; i < length; i++) {
          if (i > 0) {
            s.push(', ');
          }
          var val = value[i];
          s.push(value[i]);
        }
        return s.join('');
      } else {
        return value;
      }
    }
  }, options.gridOptions || {});

  grid = new Slick.Grid(options.$el, model, options.columns, gridOptions);
  this.grid = grid;
  grid.registerPlugin(new phantasus.AutoTooltips2());

  grid.onCellChange.subscribe(function (e, args) {
    _this.trigger('edit', args);
  });

  if (gridOptions.select) {
    grid.setSelectionModel(new Slick.RowSelectionModel({
      selectActiveRow: true,
      multiSelect: gridOptions.multiSelect
    }));
    grid.getSelectionModel().onSelectedRangesChanged.subscribe(function (e) {
      var nitems = grid.getDataLength();
      _this.trigger('selectionChanged', {
        selectedRows: grid.getSelectedRows().filter(function (row) {
          return row >= 0 && row <= nitems;
        })
      });
    });
  }

  grid.onSort.subscribe(function (e, args) {
    _this.sortCols = args.sortCols;
    _this._updateMappings();
    grid.invalidate();
  });

  options.$el.on('click', function (e) {
    var cell = grid.getCellFromEvent(e);
    if (cell) {
      _this.trigger('click', {
        row: cell.row,
        target: e.target
      });
    }
  });
  options.$el.on('dblclick', function (e) {
    var cell = grid.getCellFromEvent(e);
    if (cell) {
      _this.trigger('dblclick', {
        row: cell.row,
        target: e.target
      });
    }
  });
  if (options.sort) {
    var gridSortColumns = [];
    var gridColumns = grid.getColumns();
    var sortCols = [];
    options.sort.forEach(function (c) {
      var column = null;
      for (var i = 0; i < gridColumns.length; i++) {
        if (gridColumns[i].name === c.name) {
          column = gridColumns[i];
          break;
        }
      }
      if (column != null) {

        gridSortColumns.push({
          columnId: column.id,
          sortAsc: c.sortAsc
        });
      } else {
        // console.log(c.name + ' not found.');
      }
    });
    this.setSortColumns(gridSortColumns);
  }

  this.grid.invalidate();

};
phantasus.Grid.prototype = {
  columnsAutosized: false,
  setSortColumns: function (gridSortColumns) {
    this.grid.setSortColumns(gridSortColumns);
    this.sortCols = [];
    for (var i = 0; i < gridSortColumns.length; i++) {
      var column = this.grid.getColumns()[this.grid.getColumnIndex(gridSortColumns[i].columnId)];
      if (column == null) {
        throw 'Unable to find column ' + gridSortColumns[i];
      }
      this.sortCols.push({
        sortCol: column,
        sortAsc: gridSortColumns[i].sortAsc
      });
    }

    this._updateMappings();
    this.grid.invalidate();
  },
  setColumns: function (columns) {
    this.grid.setColumns(columns);
    this.grid.resizeCanvas();
    this.grid.invalidate();
  },
  getColumns: function () {
    return this.grid.getColumns();
  },
  getSelectedRows: function () {
    var nitems = this.grid.getDataLength();
    return this.grid.getSelectedRows().filter(function (row) {
      return row >= 0 && row <= nitems;
    });
  },
  getSelectedItems: function () {
    var rows = this.grid.getSelectedRows();
    var selection = [];
    for (var i = 0, nrows = rows.length; i < nrows; i++) {
      selection.push(this.items[this.convertViewIndexToModel(rows[i])]);
    }
    return selection;
  },
  getSelectedItem: function () {
    var rows = this.grid.getSelectedRows();
    if (rows.length === 1) {
      return this.items[this.convertViewIndexToModel(rows[0])];
    }
    return null;
  },
  /**
   * Gets the sorted, visible items
   */
  getItems: function () {
    var items = [];
    for (var i = 0, length = this.getFilteredItemCount(); i < length; i++) {
      items.push(this.items[this.convertViewIndexToModel(i)]);
    }
    return items;
  },
  getAllItemCount: function () {
    return this.items.length;
  },
  getAllItems: function () {
    return this.items;
  },
  getFilteredItemCount: function () {
    return this.viewOrder ? this.viewOrder.length : this.items.length;
  },
  redraw: function () {
    this.grid.invalidate();
  },
  redrawRows: function (rows) {
    this.grid.invalidateRows(rows);
    this.grid.render();
  },
  setItems: function (items) {
    // clear the selection
    this.items = items;
    if (this.grid.getSelectionModel()) {
      this.grid.setSelectedRows([]);
    }
    this.setFilter(this.filter);
    this.maybeAutoResizeColumns();
  },
  maybeAutoResizeColumns: function () {
    if (!this.columnsAutosized) {
      this.autosizeColumns();
    }
  },
  convertModelIndexToView: function (modelIndex) {
    if (this.modelToView !== null) {
      var index = this.modelToView.get(modelIndex);
      return index !== undefined ? index : -1;
    }
    return modelIndex;
  },
  convertViewIndexToModel: function (viewIndex) {
    return this.viewOrder != null ? (viewIndex < this.viewOrder.length
    && viewIndex >= 0 ? this.viewOrder[viewIndex] : -1) : viewIndex;
  },
  _updateMappings: function () {
    var selectedViewIndices = this.grid.getSelectionModel() != null ? this.grid
      .getSelectedRows()
      : null;
    var selectedModelIndices = [];
    if (selectedViewIndices) {
      for (var i = 0, length = selectedViewIndices.length; i < length; i++) {
        selectedModelIndices.push(this
          .convertViewIndexToModel(selectedViewIndices[i]));
      }
    }
    this.viewOrder = null;
    if (this.filter != null) {
      this.filter.init();
      if (!this.filter.isEmpty()) {
        this.viewOrder = [];
        for (var i = 0, length = this.items.length; i < length; i++) {
          if (this.filter.accept(this.items[i])) {
            this.viewOrder.push(i);
          }
        }
      }
    }
    var cols = this.sortCols;
    if (cols && cols.length > 0) {
      if (this.viewOrder == null) {
        this.viewOrder = [];
        for (var i = 0, length = this.items.length; i < length; i++) {
          this.viewOrder.push(i);
        }
      }
      var ncols = cols.length;
      var items = this.items;
      // nulls always go at end

      this.viewOrder.sort(function (index1, index2) {
        for (var i = 0; i < ncols; i++) {
          var getter = cols[i].sortCol.getter;
          var comparator = cols[i].sortAsc ? phantasus.SortKey.ASCENDING_COMPARATOR : phantasus.SortKey.DESCENDING_COMPARATOR;
          var value1 = getter(items[index1]);
          var value2 = getter(items[index2]);
          var result = comparator(value1, value2);
          if (result !== 0) {
            return result;
          }
        }
        return 0;
      });
    }
    if (this.viewOrder != null) {
      this.modelToView = new phantasus.Map();
      for (var i = 0, length = this.viewOrder.length; i < length; i++) {
        this.modelToView.set(this.viewOrder[i], i);
      }
    } else {
      this.modelToView = null;
    }
    if (this.grid.getSelectionModel() != null) {
      var newSelectedViewIndices = [];
      for (var i = 0, length = selectedModelIndices.length; i < length; i++) {
        var index = this
          .convertModelIndexToView(selectedModelIndices[i]);
        if (index !== undefined) {
          newSelectedViewIndices.push(index);
        }
      }
      this.grid.setSelectedRows(newSelectedViewIndices);
    }
  },
  setSelectedRows: function (rows) {
    this.grid.setSelectedRows(rows);
  },
  setFilter: function (filter) {
    this.filter = filter;
    this._updateMappings();
    this.grid.invalidate();
    this.trigger('filter');
  },
  getFilter: function () {
    return this.filter;
  },
  autosizeColumns: function () {
    var columns = this.grid.getColumns();
    var items = this.getItems();

    if (!items || items.length === 0 || !columns || columns.length === 0) {
      return;
    }
    var gridWidth = this.options.$el.width() - 30;
    if (gridWidth <= 0) {
      return;
    }
    this.columnsAutosized = true;
    if (columns.length > -1) {
      var div = document.createElement('div');
      document.body.appendChild(div);
      var $d = $(div);
      $d.css({
        position: 'absolute',
        left: -1000,
        top: -1000
      });

      var $row = $('<div class="slick-table">'
        + '<div class="ui-state-default slick-header-column slick-header-sortable ui-sortable-handle"></div>'
        + '<div class="ui-widget-content slick-row"><div class="slick-cell selected"></div></div>'
        + '</div>');
      var $cell = $row.find('.slick-cell');
      var $header = $row.find('.slick-header-column');
      $row.appendTo($d);

      var maxWidth = Math.min(parseInt(gridWidth / 2), 400);
      var getColumnWidth = function (column) {
        var w = $header.html(column.name).outerWidth() + 13; // leave space for sort indicator

        if (column.prototypeValue) {
          $cell.html(column.prototypeValue);
          w = Math.max($cell.outerWidth(), w);
        } else {
          for (var i = 0, nrows = Math.min(items.length, 10); i < nrows; i++) {
            var html = column.formatter(i, null, column
              .getter(items[i]), column, items[i]);
            var $html = $(html);
            $html.find('.slick-cell-wrapper').attr('class', '');
            $cell.html($html);
            w = Math.max($cell.outerWidth(), w);
          }
        }
        column.width = parseInt(Math.min(maxWidth, w));

      };
      var totalWidth = 0;
      for (var i = 0; i < columns.length; i++) {
        getColumnWidth(columns[i]);
        totalWidth += columns[i].width;
      }

      if (totalWidth < gridWidth) {
        // grow columns
        // var delta = parseInt((gridWidth - totalWidth) / columns.length);
        // for (var i = 0; i < columns.length; i++) {
        // //columns[i].width += delta;
        // }

      } else if (totalWidth > gridWidth) {
        // shrink
        //columns[columns.length - 1].width -= (totalWidth - gridWidth);
        // shrink last column
      }

      $d.remove();
      this.grid.resizeCanvas();
    }

  }
};

phantasus.Util.extend(phantasus.Grid, phantasus.Events);

/**
 * AutoTooltips2 plugin to show/hide tooltips when columns are too narrow to fit
 * content.
 *
 * @constructor
 */
phantasus.AutoTooltips2 = function (options) {
  var _grid;
  var _self = this;
  var tip;

  /**
   * Initialize plugin.
   */
  function init(grid) {
    _grid = grid;

    $(_grid.getCanvasNode()).on('mouseover', '.slick-row', showToolTip);
    $(_grid.getCanvasNode()).on('mouseout', '.slick-row', hideToolTip);
    $(_grid.getCanvasNode()).on('mouseup', hideAll);

    // $(_grid.getContainerNode()).on('mouseover', '.slick-header-column',
    // showHeaderToolTip);
    // $(_grid.getContainerNode()).on('mouseout', '.slick-header-column',
    // hideHeaderToolTip);

  }

  /**
   * Destroy plugin.
   */
  function destroy() {
    $(_grid.getCanvasNode()).off('mouseover', showToolTip);
    $(_grid.getCanvasNode()).off('mouseout', hideToolTip);
    $(_grid.getCanvasNode()).off('mouseup', hideAll);
    // $(_grid.getContainerNode()).off('mouseover', '.slick-header-column',
    // showHeaderToolTip);
    // $(_grid.getContainerNode()).off('mouseout', '.slick-header-column',
    // hideHeaderToolTip);

  }

  /**
   * Handle mouse entering grid cell to add/remove tooltip.
   *
   * @param {jQuery.Event}
   *            e - The event
   */
  function hideToolTip(e) {
    var cell = _grid.getCellFromEvent(e);
    if (cell) {
      var $node = $(_grid.getCellNode(cell.row, cell.cell));
      if ($node.data('bs.tooltip')) {
        $node.tooltip('hide');
      }
    }
  }

  function hideAll() {
    $(_grid.getCanvasNode()).find('[data-original-title]').attr(
      'data-original-title', '').tooltip('hide');

  }

  function hideHeaderToolTip(e) {
    var $node = $(e.target);
    if ($node.data('bs.tooltip')) {
      $node.tooltip('hide');
    }
  }

  function showHeaderToolTip(e) {
    var show = false;
    var $node = $(e.target);

    if (($node[0].scrollWidth > $node[0].offsetWidth)) {
      show = true;
      var $name = $node.find('.slick-column-name');
      if (!$node.data('bs.tooltip')) {
        $node.tooltip({
          placement: 'auto',
          html: true,
          container: 'body',
          trigger: 'manual'
        });
      }
      $node.attr('data-original-title', $name.text());
      if (show) {
        $node.tooltip('show');
      } else {
        $node.tooltip('hide');
      }
    }
  }

  function showToolTip(e) {
    var cell = _grid.getCellFromEvent(e);
    if (cell) {
      var $node = $(_grid.getCellNode(cell.row, cell.cell));
      var text = '';
      var c = _grid.getColumns()[cell.cell];
      var show = false;
      var $checkNode = $node.find('.slick-cell-wrapper');
      if (c.alwaysShowTooltip
        || ($checkNode[0].scrollWidth > $checkNode[0].offsetWidth)) {
        var item = _grid.getDataItem(cell.row);
        text = c.tooltip(item, c.getter(item));
        show = true;
      }
      $node.attr('data-original-title', text);
      var hasTip = $node.data('bs.tooltip');
      if (!hasTip) {
        $node.tooltip({
          placement: 'auto',
          html: true,
          container: 'body',
          trigger: 'manual'
        });
      }
      if (show) {
        $node.tooltip('show');
      } else if (hasTip) {
        $node.tooltip('hide');
      }
    }
  }

  /**
   * Handle mouse entering header cell to add/remove tooltip.
   *
   * @param {jQuery.Event}
   *            e - The event
   * @param {object}
   *            args.column - The column definition
   */
  function handleHeaderMouseEnter(e, args) {
    var column = args.column, $node = $(e.target).closest(
      '.slick-header-column');
    if (!column.toolTip) {
      $node.attr('title',
        ($node.innerWidth() < $node[0].scrollWidth) ? column.name
          : '');
    }
  }

  // Public API
  $.extend(this, {
    'init': init,
    'destroy': destroy
  });

};

phantasus.CombinedGridFilter = function () {
  this.filters = [];
};
phantasus.CombinedGridFilter.prototype = {
  add: function (filter) {
    this.filters.push(filter);
  },
  getFilters: function () {
    return this.filters;
  },
  get: function (index) {
    return this.filters[index];
  },
  set: function (index, f) {
    this.filters[index] = f;
  },
  init: function () {
    for (var i = 0; i < this.filters.length; i++) {
      this.filters[i].init();
    }

    this.activeFilters = this.filters.filter(function (f) {
      return !f.isEmpty();
    });
    this.nActiveFilters = this.activeFilters.length;
  },
  accept: function (item) {
    for (var i = 0; i < this.nActiveFilters; i++) {
      if (!this.activeFilters[i].accept(item)) {
        return false;
      }
    }
    return true;
  },
  isEmpty: function () {
    return this.activeFilters.length === 0;
  }
};

phantasus.HeatMapColorSchemeChooser = function (options) {
  var _this = this;
  this.$div = $('<div></div>');
  this.currentValue = null;
  this.legend = new phantasus.LegendWithStops();
  this.colorScheme = options.colorScheme || new phantasus.HeatMapColorScheme(new phantasus.Project(new phantasus.Dataset({
      rows: 0,
      columns: 0
    })));
  this.legend.on('added', function (e) {
    var fractions = _this.colorScheme.getFractions();
    fractions.push(e.fraction);
    var colors = _this.colorScheme.getColors();
    colors.push('black');
    _this.colorScheme.setFractions({
      fractions: fractions,
      colors: colors
    });
    var newIndex = _this.getFractionIndex(e.fraction, 'black');
    _this.setSelectedIndex(newIndex);
    _this.fireChanged();
  }).on('selectedIndex', function (e) {
    _this.setSelectedIndex(e.selectedIndex);
  }).on('delete', function (index) {
    _this.deleteSelectedStop();
  }).on(
    'moved',
    function (e) {
      var fraction = e.fraction;
      var fractions = _this.colorScheme.getFractions();
      fractions[_this.legend.selectedIndex] = fraction;
      var color = _this.colorScheme.getColors()[_this.legend.selectedIndex];
      _this.colorScheme.setFractions({
        fractions: fractions,
        colors: _this.colorScheme.getColors()
      });
      _this.legend.selectedIndex = _this.getFractionIndex(e.fraction, color);
      var fractionToValue = d3.scale.linear().domain([0, 1])
        .range(
          [_this.colorScheme.getMin(),
            _this.colorScheme.getMax()])
        .clamp(true);
      _this.formBuilder.setValue('selected_value',
        fractionToValue(fractions[_this.legend.selectedIndex]));
      _this.fireChanged();
    });
  var $row = $('<div></div>');
  $row.css('height', '50px').css('width', '300px').css('margin-left', 'auto')
    .css('margin-right', 'auto');
  $row.appendTo(this.$div);

  $(this.legend.canvas).appendTo($row);
  var formBuilder = new phantasus.FormBuilder();
  var items = [];
  items = items.concat({
    name: 'selected_color',
    type: 'color',
    style: 'max-width: 50px;'
  }, {
    name: 'selected_value',
    type: 'text',
    style: 'max-width: 100px;'
  }, [{
    name: 'delete',
    type: 'button',
    value: 'Delete Selected Color Stop',
  }, {
    name: 'add',
    type: 'button',
    value: 'Add Color Stop'
  }], {
    name: 'minimum',
    type: 'text',
    style: 'max-width: 100px;'
  }, {
    name: 'maximum',
    type: 'text',
    style: 'max-width: 100px;'
  });
  if (options.showRelative) {
    items = items.concat({
      name: 'relative_color_scheme',
      type: 'checkbox',
      help: 'A relative color scheme uses the minimum and maximum values in each row' +
      ' to convert values to colors'
    });
    items = items.concat({
      name: 'transform_values',
      type: 'select',
      value: 0,
      options: [{
        name: 'None',
        value: 0
      }, {
        name: 'Subtract row mean, divide by row standard deviation',
        value: phantasus.AbstractColorSupplier.Z_SCORE
      }, {
        name: 'Subtract row median, divide by row median absolute deviation',
        value: phantasus.AbstractColorSupplier.ROBUST_Z_SCORE
      }]
    });
  }

  items = items.concat({
    name: 'missing_color',
    type: 'color',
    style: 'max-width: 50px;'
  });
  items
    .push({
      name: 'stepped_colors',
      type: 'checkbox',
      value: false,
      help: 'Intervals include left end point and exclude right end point, except for the highest interval'
    });
  _.each(items, function (item) {
    formBuilder.append(item);
  });
  this.getFractionIndex = function (fraction, color) {
    var fractions = _this.colorScheme.getFractions();
    var colors = _this.colorScheme.getColors();
    for (var i = 0, len = fractions.length; i < len; i++) {
      if (fractions[i] === fraction && colors[i] === color) {
        return i;
      }
    }
    return -1;
  };
  this.$div.append(formBuilder.$form);
  formBuilder.$form.find('[name^=selected],[name=delete]').prop('disabled',
    true);
  formBuilder.$form.find('[name=add]').on('click', function (e) {
    var fractions = _this.colorScheme.getFractions();
    var val = 0.5;
    while (val >= 0 && _.indexOf(fractions, val) !== -1) {
      val -= 0.1;
    }
    val = Math.max(0, val);
    fractions.push(val);
    var colors = _this.colorScheme.getColors();
    colors.push('black');
    _this.colorScheme.setFractions({
      fractions: fractions,
      colors: colors
    });
    var newIndex = _this.getFractionIndex(e.fraction, 'black');
    _this.setSelectedIndex(newIndex);
    _this.fireChanged();
  });
  formBuilder.$form.find('[name=delete]').on('click', function (e) {
    _this.deleteSelectedStop();
  });
  formBuilder.$form.find('[name=transform_values]').on('change', function (e) {
    _this.colorScheme.setTransformValues(parseInt(formBuilder.getValue('transform_values')));
    _this.fireChanged();
  });
  formBuilder.$form.on('keyup', '[name=selected_value]', _.debounce(function (e) {
    var val = parseFloat($(this).val());
    if (!isNaN(val)) {
      _this.setSelectedValue(val);
      _this.fireChanged();
    }
  }, 100));
  formBuilder.$form.on('change', '[name=selected_color]', function (e) {
    var colors = _this.colorScheme.getColors();
    colors[_this.legend.selectedIndex] = $(this).val();
    _this.colorScheme.setFractions({
      fractions: _this.colorScheme.getFractions(),
      colors: colors
    });
    _this.fireChanged();
  });
  formBuilder.$form.on('change', '[name=missing_color]', function (e) {
    var color = $(this).val();
    _this.colorScheme.setMissingColor(color);
    _this.fireChanged(false);
  });
  formBuilder.$form.on('change', '[name=stepped_colors]', function (e) {
    _this.colorScheme.setStepped($(this).prop('checked'));
    _this.fireChanged();
  });
  formBuilder.$form.on('keyup', '[name=minimum]', _.debounce(function (e) {
    var val = parseFloat($(this).val());
    if (!isNaN(val)) {
      _this.colorScheme.setMin(val);
      _this.setSelectedIndex(_this.legend.selectedIndex);
      _this.fireChanged(false);
    }
  }, 100));
  formBuilder.$form.on('keyup', '[name=maximum]', _.debounce(function (e) {
    var val = parseFloat($(this).val());
    if (!isNaN(val)) {
      _this.colorScheme.setMax(val);
      _this.setSelectedIndex(_this.legend.selectedIndex);
      _this.fireChanged(false);
    }

  }, 100));
  formBuilder.$form
    .on(
      'change',
      '[name=relative_color_scheme]',
      _
        .throttle(
          function (e) {
            _this.legend.selectedIndex = -1;
            // FIXME set fixed min and max
            var scalingMode = $(this).prop('checked') ? phantasus.HeatMapColorScheme.ScalingMode.RELATIVE
              : phantasus.HeatMapColorScheme.ScalingMode.FIXED;
            _this.colorScheme
              .setScalingMode(scalingMode);
            _this.setColorScheme(_this.colorScheme);
            _this.fireChanged();
          }, 100));
  this.formBuilder = formBuilder;
  // selection: delete, color, value
  // general: add, min, max, relative or global
};
phantasus.HeatMapColorSchemeChooser.prototype = {
  deleteSelectedStop: function () {
    var fractions = this.colorScheme.getFractions();
    fractions.splice(this.legend.selectedIndex, 1);
    var colors = this.colorScheme.getColors();
    colors.splice(this.legend.selectedIndex, 1);
    this.colorScheme.setFractions({
      fractions: fractions,
      colors: colors
    });
    this.formBuilder.$form.find('[name^=selected],[name=delete]').prop(
      'disabled', true);
    this.legend.setSelectedIndex(-1);
    this.fireChanged();
  },
  setSelectedValue: function (val) {
    var valueToFraction = d3.scale.linear().domain(
      [this.colorScheme.getMin(), this.colorScheme.getMax()])
      .range([0, 1]).clamp(true);
    var fractions = this.colorScheme.getFractions();
    var fraction = valueToFraction(val);
    fractions[this.legend.selectedIndex] = fraction;
    var color = this.colorScheme.getColors()[this.legend.selectedIndex];
    this.colorScheme.setFractions({
      fractions: fractions,
      colors: this.colorScheme.getColors()
    });
    this.legend.selectedIndex = this.getFractionIndex(fraction, color);
  },
  setSelectedIndex: function (index) {
    var fractions = this.colorScheme.getFractions();
    if (index >= fractions.length) {
      index = -1;
    }
    this.legend.setSelectedIndex(index);
    var formBuilder = this.formBuilder;
    formBuilder.$form.find('[name^=selected],[name=delete]').prop(
      'disabled', this.legend.selectedIndex === -1);
    if (this.legend.selectedIndex !== -1) {
      var fractionToValue = d3.scale.linear().domain([0, 1]).range(
        [this.colorScheme.getMin(), this.colorScheme.getMax()])
        .clamp(true);
      formBuilder.setValue('selected_value',
        fractionToValue(fractions[this.legend.selectedIndex]));
      var context = this.legend.canvas.getContext('2d');
      var colors = this.colorScheme.getColors();
      context.fillStyle = colors[this.legend.selectedIndex];
      formBuilder.setValue('selected_color', context.fillStyle);
    } else {
      formBuilder.setValue('selected_value', '');
    }
    this.draw();
  },
  setMinMax: function () {
    if (this.colorScheme.getScalingMode() === phantasus.HeatMapColorScheme.ScalingMode.RELATIVE) {
      this.colorScheme.setMin(0);
      this.colorScheme.setMax(1);
    }
  },
  dispose: function () {
    this.off('change');
    this.legend.destroy();
    this.formBuilder.$form.off('keyup', 'input');
    this.formBuilder.$form.off('change', '[name=relative_color_scheme]');
  },
  restoreCurrentValue: function () {
    if (this.colorScheme.setCurrentValue) {
      this.colorScheme.setCurrentValue(this.currentValue);
    }
  },
  setCurrentValue: function (value) {
    this.currentValue = value;
    if (this.colorScheme && this.colorScheme.setCurrentValue) {
      this.colorScheme.setCurrentValue(this.currentValue);
    }
    this.setColorScheme(this.colorScheme);
  },
  setColorScheme: function (colorScheme) {
    this.colorScheme = colorScheme;
    this.setMinMax();
    if (colorScheme.setCurrentValue) {
      colorScheme.setCurrentValue(this.currentValue);
    }
    this.formBuilder
      .setValue(
        'relative_color_scheme',
        colorScheme.getScalingMode() === phantasus.HeatMapColorScheme.ScalingMode.RELATIVE ? true
          : false);
    this.formBuilder.setValue('transform_values', colorScheme.getTransformValues());
    this.formBuilder.setEnabled('transform_values', colorScheme.getScalingMode() !== phantasus.HeatMapColorScheme.ScalingMode.RELATIVE);

    this.formBuilder.$form
      .find('[name=minimum],[name=maximum]')
      .prop(
        'disabled',
        colorScheme.getScalingMode() === phantasus.HeatMapColorScheme.ScalingMode.RELATIVE);
    this.formBuilder.setValue('minimum', this.colorScheme.getMin());
    this.formBuilder.setValue('maximum', this.colorScheme.getMax());
    this.formBuilder.setValue('stepped_colors', this.colorScheme
      .isStepped());
    this.formBuilder.setValue('missing_color', this.colorScheme
      .getMissingColor());
    this.draw();
  },
  getFractionToStopPix: function () {
    return d3.scale.linear().clamp(true).domain([0, 1]).range(
      [this.legend.border,
        this.legend.getUnscaledWidth() - this.legend.border]);
  },
  fireChanged: function (noreset) {
    this.trigger('change');
    if (noreset !== false) {
      this.setColorScheme(this.colorScheme);
    }
  },
  draw: function () {
    var colorScheme = this.colorScheme;
    if (colorScheme.getScalingMode() === phantasus.HeatMapColorScheme.ScalingMode.RELATIVE) {
      colorScheme.setMin(0);
      colorScheme.setMax(1);
    }
    var fractions = colorScheme.getFractions();
    var colors = colorScheme.getColors();
    var fractionToStopPix = this.getFractionToStopPix();
    this.legend.draw(fractions, colors, colorScheme.isStepped(),
      fractionToStopPix);
  }
};
phantasus.Util.extend(phantasus.HeatMapColorSchemeChooser, phantasus.Events);

phantasus.HeatMapColorSchemeLegend = function (heatMap, $keyContent) {
  var colorScheme = heatMap.heatmap.getColorScheme();
  var colorByValues = colorScheme.getColorByValues();
  var totalHeight;
  $keyContent.empty();
  var ntracks = colorByValues.length;
  colorByValues
    .forEach(function (value) {
      if (value != null || ntracks === 1) {
        if (value != 'null') { // values are stored as string
          var $label = $('<div style="overflow:hidden;text-overflow:' +
            ' ellipsis;width:250px;max-width:250px;">'
            + value + '</div>');
          $keyContent.append($label);
          totalHeight += $label.height();
        }
        var trackLegend = new phantasus.ColorSupplierLegend(
          colorScheme, value);
        $(trackLegend.canvas).css('position', '');
        trackLegend.repaint();
        trackLegend.on('selectionChanged', function () {
          heatMap.heatmap.setInvalid(true);
          heatMap.heatmap.repaint();
        });
        $keyContent.append($(trackLegend.canvas));
        totalHeight += trackLegend.getUnscaledHeight();
      }
    });
  if (heatMap.options.$key) {
    $keyContent.append(heatMap.options.$key);
    totalHeight += heatMap.options.$key.height();

  }
  var $edit = $('<div style="padding-left:4px; display:inline;"><a data-name="options"' +
    ' href="#">Edit</a></div>');

  $edit.find('[data-name=options]').on('click', function (e) {
    e.preventDefault();
    heatMap.showOptions();
    phantasus.Util.trackEvent({
      eventCategory: 'ToolBar',
      eventAction: 'options'
    });
  });
  totalHeight += $edit.height();
  $keyContent.append($edit);
  $keyContent.css({
    'text-overflow': 'ellipsis',
    overflow: 'hidden',
    width: 250 + 'px',
    height: totalHeight + 'px'
  });
};

phantasus.HeatMapColorSchemeLegend.drawColorScheme = function (context,
                                                              colorScheme, width, printing, hideText, legendHeight) {
  if (!legendHeight) {
    legendHeight = 12;
  }
  context.font = '11px ' + phantasus.CanvasUtil.getFontFamily(context);
  var names = colorScheme.getNames();
  var hasNames = names != null;
  // if hasNames that we draw vertically to ensure space for names
  if (hasNames) {
    phantasus.HeatMapColorSchemeLegend.drawColorSchemeVertically(context,
      colorScheme, colorScheme.getHiddenValues(), printing);
  } else {
    phantasus.HeatMapColorSchemeLegend.draw(context, colorScheme
        .getFractions(), colorScheme.getColors(), width, legendHeight,
      colorScheme.isStepped());
    context.strokeStyle = 'LightGrey';
    context.strokeRect(0, 0, width, legendHeight);
    if (hideText) {
      return;
    }
    var map = d3.scale.linear().domain([0, 1]).range([0, width]).clamp(
      true);
    var fractionToValue = d3.scale.linear().domain([0, 1]).range(
      [colorScheme.getMin(), colorScheme.getMax()]).clamp(true);
    context.textAlign = 'center';
    context.textBaseline = 'top';
    context.fillStyle = 'black';

    if (colorScheme.getScalingMode() === phantasus.HeatMapColorScheme.ScalingMode.RELATIVE) {
      context.fillText('row min', 0, 14);
      context.fillText('row max', width, legendHeight + 2);
    } else {
      var fractions = colorScheme.getFractions();
      var lastTextPixEnd = -1;
      // draw from left to middle and then from right to middle to avoid
      // text overlap
      var halfway = parseInt(fractions.length / 2);

      for (var i = 0; i < halfway; i++) {
        var pix = map(fractions[i]);
        var text = '';
        if (hasNames) {
          text = names[i] != '' ? (names[i] + ' ('
          + fractionToValue(fractions[i]) + ')') : names[i];
        } else {
          text = phantasus.Util.nf(fractionToValue(fractions[i]));
        }
        var textWidth = context.measureText(text).width;
        if (pix > lastTextPixEnd) {
          context.fillText(text, pix, legendHeight + 2);
        }
        lastTextPixEnd = pix + textWidth / 2;
      }
      var lastTextPixStart = 10000;
      for (var i = fractions.length - 1; i >= halfway; i--) {
        var pix = map(fractions[i]);
        var text = '';
        if (hasNames) {
          text = names[i] != '' ? (names[i] + ' ('
          + fractionToValue(fractions[i]) + ')') : names[i];
        } else {
          text = phantasus.Util.nf(fractionToValue(fractions[i]));
        }

        var textWidth = context.measureText(text).width;
        var textPixEnd = pix + textWidth / 2;
        if (textPixEnd < lastTextPixStart) {
          context.fillText(text, pix, legendHeight + 2);
          lastTextPixStart = pix - textWidth / 2;
        }
      }
    }
  }
};
phantasus.HeatMapColorSchemeLegend.drawColorSchemeVertically = function (context,
                                                                        colorScheme, hiddenValues, printing) {
  var fractionToValue = d3.scale.linear().domain([0, 1]).range(
    [colorScheme.getMin(), colorScheme.getMax()]).clamp(true);
  context.textAlign = 'left';
  context.textBaseline = 'top';
  context.fillStyle = 'black';
  var fractions = colorScheme.getFractions();
  var colors = colorScheme.getColors();
  var names = colorScheme.getNames();
  context.strokeStyle = 'LightGrey';
  var xpix = 0;
  var ypix = 0;

  context.font = '12px ' + phantasus.CanvasUtil.getFontFamily(context);
  for (var i = 0; i < colors.length; i++) {
    var name = names[i];
    if (name != null) {
      context.fillStyle = colors[i];
      context.fillRect(xpix, ypix, 12, 12);
      context.strokeRect(xpix, ypix, 12, 12);
      context.fillStyle = phantasus.CanvasUtil.FONT_COLOR;
      if (hiddenValues && !printing) {
        var value = fractionToValue(fractions[i]);
        context.font = '12px FontAwesome';
        if (!hiddenValues.has(value)) {
          context.fillText('\uf00c', -14, ypix); // checked
        }
        // else {
        // context.fillText("\uf096", -14, ypix); // unchecked
        // }
        context.font = '12px ' + phantasus.CanvasUtil.getFontFamily(context);
      }
      context.fillText(name, xpix + 16, ypix);
    }
    ypix += 14;
  }
};
phantasus.HeatMapColorSchemeLegend.draw = function (context, fractions, colors,
                                                   width, height, stepped) {
  if (!stepped) {
    var gradient = context.createLinearGradient(0, 0, width, height);
    for (var i = 0, length = fractions.length; i < length; i++) {
      gradient.addColorStop(fractions[i], colors[i]);
    }
    context.fillStyle = gradient;
    context.fillRect(0, 0, width, height);
  } else {
    // intervals include left end point, exclude right end point, except for
    // the highest interval
    // TODO right-most endpoint is not shown
    var map = d3.scale.linear().domain([0, 1]).range([0, width]).clamp(
      true);
    for (var i = 0, length = fractions.length; i < length; i++) {
      context.fillStyle = colors[i];
      var x1 = map(fractions[i]);
      var x2 = i === length - 1 ? width : map(fractions[i + 1]);
      context.fillRect(Math.min(x1, x2), 0, Math.abs(x2 - x1), height);
    }
  }
};

phantasus.ColorSupplierLegend = function (colorScheme, value) {
  phantasus.AbstractCanvas.call(this, false);
  var _this = this;
  this.value = value;
  this.colorScheme = colorScheme;
  colorScheme.setCurrentValue(value);
  var hiddenValues = colorScheme.getHiddenValues();

  var names = colorScheme.getNames();
  var hasNames = names != null;
  var legendHeight = hasNames ? names.length * 14 : 30;
  var bounds = {
    width: 250,
    height: legendHeight
  };
  this.hasNames = hasNames;
  this.setBounds(bounds);
  if (hasNames && hiddenValues) {
    $(this.canvas)
      .on(
        'click',
        function (e) {
          e.preventDefault();
          e.stopPropagation();
          var clickedRow = Math
            .floor((e.clientY - _this.canvas
                .getBoundingClientRect().top) / 14);
          var fractionToValue = d3.scale.linear().domain(
            [0, 1]).range(
            [colorScheme.getMin(),
              colorScheme.getMax()]).clamp(true);
          var fractions = colorScheme.getFractions();
          var value = fractionToValue(fractions[clickedRow]);
          if (!hiddenValues.has(value)) {
            hiddenValues.add(value);
          } else {
            hiddenValues.remove(value);

          }
          _this.trigger('selectionChanged');
          _this.repaint();
        });
  }
};

phantasus.ColorSupplierLegend.prototype = {
  draw: function (clip, context) {
    var colorScheme = this.colorScheme;
    colorScheme.setCurrentValue(this.value);
    // context.fillStyle = 'white';
    // context.fillRect(0, 0, this.getUnscaledWidth(), this
    // .getUnscaledHeight());
    context.translate(this.hasNames ? 14
      : (this.getUnscaledWidth() - 200) / 2, 0);
    phantasus.HeatMapColorSchemeLegend.drawColorScheme(context, colorScheme,
      200);

  }

};
phantasus.Util.extend(phantasus.ColorSupplierLegend, phantasus.Events);
phantasus.Util.extend(phantasus.ColorSupplierLegend,
  phantasus.AbstractCanvas);






/**
 * @param type
 *            Either relative or fixed.
 * @param stops
 *            An array of objects with value and color
 */
phantasus.HeatMapColorScheme = function (project, scheme) {
  this.project = project;
  var _this = this;

  this.separateColorSchemeForRowMetadataField = null;
  this.rowValueToColorSupplier = {};
  this.value = null;
  if (scheme) {
    if (scheme.valueToColorScheme) { // json representation
      this.fromJSON(scheme);
    } else {
      this.rowValueToColorSupplier[null] = this.fromJSON(scheme);
      this.currentColorSupplier = this.rowValueToColorSupplier[this.value];
    }
  }
  project
    .on(
      'rowFilterChanged columnFilterChanged rowSortOrderChanged columnSortOrderChanged datasetChanged',
      function () {
        _this.projectUpdated();
      });
  this.projectUpdated();
};
phantasus.HeatMapColorScheme.Predefined = {};

phantasus.HeatMapColorScheme.Predefined.CN = function () {
  return {
    scalingMode: 'fixed',
    values: [-2, -0.1, 0.1, 2],
    colors: ['#0000ff', '#ffffff', '#ffffff', '#ff0000']
  };
};
phantasus.HeatMapColorScheme.Predefined.BINARY = function () {
  return {
    scalingMode: 'fixed',
    values: [0, 1],
    colors: ['#ffffff', 'black']
  };
};
phantasus.HeatMapColorScheme.Predefined.RELATIVE = function () {
  return {
    scalingMode: 'relative'
  };
};
phantasus.HeatMapColorScheme.Predefined.MAF = function () {
  // coMut plot colors
  return {
    scalingMode: 'fixed',
    stepped: true,
    values: [0, 1, 2, 3, 4, 5, 6, 7],
    names: ['', 'Synonymous', 'In Frame Indel', 'Other Non-Synonymous', 'Missense', 'Splice Site', 'Frame Shift', 'Nonsense'],
    colors: ['#ffffff', '#4daf4a', '#ffff33', '#a65628', '#377eb8', '#984ea3', '#ff7f00', '#e41a1c']
  };
};
// phantasus.HeatMapColorScheme.Predefined.MAF_NEW = function() {
// // Synonymous 1
// //In_frame_Indel 2
// //Other_non_syn. 3
// //Missense 4
// //Splice_Site 5
// //Frame_Shift 6
// //Nonsense 7
// return {
// type : 'fixed',
// stepped : true,
// map : [ {
// value : 0,
// color : 'rgb(' + [ 255, 255, 255 ].join(',') + ')',
// name : ''
// }, {
// value : 1,
// color : 'rgb(' + [ 255, 255, 179 ].join(',') + ')',
// name : 'Silent'
// }, {
// value : 2,
// color : 'rgb(' + [ 69, 117, 180 ].join(',') + ')',
// name : 'In Frame Indel'
// }, {
// value : 3,
// color : 'rgb(' + [ 247, 182, 210 ].join(',') + ')',
// name : 'Other Non-Synonymous'
// }, {
// value : 4,
// color : 'rgb(' + [ 1, 133, 113 ].join(',') + ')',
// name : 'Missense'
// }, {
// value : 5,
// color : 'rgb(' + [ 253, 180, 98 ].join(',') + ')',
// name : 'Splice Site'
// }, {
// value : 6,
// color : 'rgb(' + [ 140, 81, 10 ].join(',') + ')',
// name : 'Frame Shift'
// }, {
// value : 7,
// color : 'rgb(' + [ 123, 50, 148 ].join(',') + ')',
// name : 'Nonsense'
// } ]
// };
// };
phantasus.HeatMapColorScheme.Predefined.ZS = function () {
  return {
    scalingMode: 'fixed',
    values: [-10, -2, 2, 10],
    colors: ['#0000ff', '#ffffff', '#ffffff', '#ff0000']
  };
};
phantasus.HeatMapColorScheme.ScalingMode = {
  RELATIVE: 0,
  FIXED: 1
};

phantasus.HeatMapConditions = function () {
  this.array = [];
  // each condition is a object with: seriesName (series is old deprecated field), shape, color and
  // accept(val) function

};
phantasus.HeatMapConditions.prototype = {
  insert: function (index, c) {
    this.array.splice(index, 0, c);
  },
  add: function (c) {
    this.array.push(c);
  },
  getConditions: function () {
    return this.array;
  },
  remove: function (index) {
    this.array.splice(index, 1);
  },
  copy: function () {
    var c = new phantasus.HeatMapConditions();
    this.array.forEach(function (cond) {
      c.array.push(_.clone(cond));
    });
    return c;
  }
};

phantasus.HeatMapColorScheme.prototype = {
  getColors: function () {
    return this.currentColorSupplier.getColors();
  },
  setMissingColor: function (color) {
    this.currentColorSupplier.setMissingColor(color);
  },
  getHiddenValues: function () {
    return this.currentColorSupplier.getHiddenValues ? this.currentColorSupplier
        .getHiddenValues()
      : null;
  },
  getMissingColor: function () {
    return this.currentColorSupplier.getMissingColor();
  },
  getScalingMode: function () {
    return this.currentColorSupplier.getScalingMode();
  },
  getSizer: function () {
    return this.currentColorSupplier.getSizer();
  },
  getConditions: function () {
    return this.currentColorSupplier.getConditions();
  },
  setScalingMode: function (scalingMode) {
    this.currentColorSupplier.setScalingMode(scalingMode);
  },
  getFractions: function () {
    return this.currentColorSupplier.getFractions();
  },
  getNames: function () {
    return this.currentColorSupplier.getNames();
  },
  getMin: function () {
    return this.currentColorSupplier.getMin();
  },
  getMax: function () {
    return this.currentColorSupplier.getMax();
  },
  setMin: function (min) {
    this.currentColorSupplier.setMin(min);
  },
  setMax: function (max) {
    this.currentColorSupplier.setMax(max);
  },
  isStepped: function () {
    return this.currentColorSupplier.isStepped();
  },
  setFractions: function (options) {
    this.currentColorSupplier.setFractions(options);
  },
  setTransformValues: function (options) {
    this.currentColorSupplier.setTransformValues(options);
    this.cachedRowStats.cachedRow = -1;
  },
  getTransformValues: function () {
    return this.currentColorSupplier.getTransformValues();
  },
  setStepped: function (stepped) {
    var oldColorSupplier = this.currentColorSupplier;
    var newColorSupplier = stepped ? new phantasus.SteppedColorSupplier()
      : new phantasus.GradientColorSupplier();
    newColorSupplier.sizer = oldColorSupplier.getSizer();
    newColorSupplier.array = oldColorSupplier.getConditions();
    newColorSupplier.setScalingMode(oldColorSupplier.getScalingMode());
    newColorSupplier.setMin(oldColorSupplier.getMin());
    newColorSupplier.setMax(oldColorSupplier.getMax());
    newColorSupplier.setFractions({
      fractions: oldColorSupplier.getFractions(),
      colors: oldColorSupplier.getColors()
    });
    this.currentColorSupplier = newColorSupplier;
    this.rowValueToColorSupplier[this.value] = this.currentColorSupplier;
  },
  toJSON: function () {
    var json = {};
    var _this = this;
    if (this.separateColorSchemeForRowMetadataField != null) {
      json.separateColorSchemeForRowMetadataField = this.separateColorSchemeForRowMetadataField;
    }
    json.valueToColorScheme = {};
    _.each(_.keys(this.rowValueToColorSupplier), function (key) {
      // save each scheme
      json.valueToColorScheme[key] = phantasus.AbstractColorSupplier.toJSON(_this.rowValueToColorSupplier[key]);
    });

    return json;
  },
  fromJSON: function (json) {
    var _this = this;
    if (json.separateColorSchemeForRowMetadataField) {
      this.separateColorSchemeForRowMetadataField = json.separateColorSchemeForRowMetadataField;
      this.vector = this.project.getSortedFilteredDataset()
        .getRowMetadata().getByName(
          this.separateColorSchemeForRowMetadataField);
    }
    this.rowValueToColorSupplier = {};
    var obj = json.valueToColorScheme || json.colorSchemes;
    if (obj == null) {
      var colorSupplier = phantasus.AbstractColorSupplier
        .fromJSON(json);
      _this.rowValueToColorSupplier['null'] = colorSupplier;
    } else {
      _.each(_.keys(obj), function (key) {
        var colorSupplier = phantasus.AbstractColorSupplier
          .fromJSON(obj[key]);
        _this.rowValueToColorSupplier[key] = colorSupplier;
      });
    }
    this._ensureColorSupplierExists();

  },
  copy: function (project) {
    var _this = this;
    var c = new phantasus.HeatMapColorScheme(project);
    c.separateColorSchemeForRowMetadataField = this.separateColorSchemeForRowMetadataField;
    if (c.separateColorSchemeForRowMetadataField != null) {
      c.vector = project.getSortedFilteredDataset().getRowMetadata()
        .getByName(c.separateColorSchemeForRowMetadataField);

    }
    if (c.vector == null) {
      c.separateColorSchemeForRowMetadataField = null;
    }
    _.each(_.keys(this.rowValueToColorSupplier), function (key) {
      c.rowValueToColorSupplier[key] = _this.rowValueToColorSupplier[key]
        .copy();
    });

    c.value = this.value;
    c.currentColorSupplier = c.rowValueToColorSupplier[c.value];

    return c;
  },
  setSeparateColorSchemeForRowMetadataField: function (separateColorSchemeForRowMetadataField) {
    if (separateColorSchemeForRowMetadataField != this.separateColorSchemeForRowMetadataField) {
      this.separateColorSchemeForRowMetadataField = separateColorSchemeForRowMetadataField;
      this.vector = this.project.getSortedFilteredDataset()
        .getRowMetadata().getByName(
          separateColorSchemeForRowMetadataField);
      var _this = this;
      _.each(_.keys(this.rowValueToColorSupplier), function (key) {
        // remove old color schemes
        delete _this.rowValueToColorSupplier[key];
      });
    }
  },
  getProject: function () {
    return this.project;
  },
  getSeparateColorSchemeForRowMetadataField: function () {
    return this.separateColorSchemeForRowMetadataField;
  },
  getColorByValues: function () {
    return _.keys(this.rowValueToColorSupplier);
  },
  projectUpdated: function () {
    var dataset = this.project.getSortedFilteredDataset();
    if (this.separateColorSchemeForRowMetadataField != null) {
      this.vector = this.project.getSortedFilteredDataset()
        .getRowMetadata().getByName(
          this.separateColorSchemeForRowMetadataField);
    }
    this.cachedRowStats = new phantasus.RowStats(dataset);
  },
  setColorSupplierForCurrentValue: function (colorSupplier) {
    this.rowValueToColorSupplier[this.value] = colorSupplier;
    this.currentColorSupplier = colorSupplier;
  },
  setCurrentValue: function (value) {
    this.value = value;
    this._ensureColorSupplierExists();
  },
  isSizeBy: function () {
    this.currentColorSupplier.isSizeBy();
  },
  getCurrentColorSupplier: function () {
    return this.currentColorSupplier;
  },
  getColor: function (row, column, val) {
    if (this.vector !== undefined) {
      var tmp = this.vector.getValue(row);
      if (this.value !== tmp) {
        this.value = tmp;
        this._ensureColorSupplierExists();
      }
    }
    if (this.currentColorSupplier.getScalingMode() === phantasus.HeatMapColorScheme.ScalingMode.RELATIVE) {
      if (this.cachedRowStats.maybeUpdateRelative(row)) {
        this.currentColorSupplier
          .setMin(this.cachedRowStats.rowCachedMin);
        this.currentColorSupplier
          .setMax(this.cachedRowStats.rowCachedMax);
      }
    } else if (this.currentColorSupplier.getTransformValues() && this.cachedRowStats.cachedRow !== row) {
      this.cachedRowStats.cacheTransformValues(row, this.currentColorSupplier.getTransformValues());
      val = (val - this.cachedRowStats.rowCachedMean) / this.cachedRowStats.rowCachedStandardDeviation;
    }
    return this.currentColorSupplier.getColor(row, column, val);
  },
  /**
   * @private
   */
  _ensureColorSupplierExists: function () {
    this.currentColorSupplier = this.rowValueToColorSupplier[this.value];
    if (this.currentColorSupplier === undefined) {
      var cs = phantasus.AbstractColorSupplier.fromJSON({
        scalingMode: 'relative'
      });
      this.rowValueToColorSupplier[this.value] = cs;
      this.currentColorSupplier = cs;
    }
  }
};
phantasus.RowStats = function (dataset) {
  this.datasetRowView = new phantasus.DatasetRowView(dataset);
  this.cachedRow = -1;
  this.rowCachedMax = 0;
  this.rowCachedMin = 0;
  this.rowCachedStandardDeviation = -1;
  this.rowCachedMean = -1;
};
phantasus.RowStats.prototype = {
  cacheTransformValues: function (row, transform) {
    var meanFunction = transform === phantasus.AbstractColorSupplier.Z_SCORE ? phantasus.Mean : phantasus.Median;
    var stdevFunction = transform === phantasus.AbstractColorSupplier.Z_SCORE ? phantasus.StandardDeviation : phantasus.MAD;
    this.datasetRowView.setIndex(row);
    this.rowCachedMean = meanFunction(this.datasetRowView);
    this.rowCachedStandardDeviation = stdevFunction(this.datasetRowView, this.rowCachedMean);
  },
  maybeUpdateRelative: function (row) {
    if (this.cachedRow !== row) {
      this.cachedRow = row;
      this.datasetRowView.setIndex(row);
      this.rowCachedMax = -Number.MAX_VALUE;
      this.rowCachedMin = Number.MAX_VALUE;
      for (var j = 0, ncols = this.datasetRowView.size(); j < ncols; j++) {
        var d = this.datasetRowView.getValue(j);
        if (!isNaN(d)) {
          this.rowCachedMax = d > this.rowCachedMax ? d
            : this.rowCachedMax;
          this.rowCachedMin = d < this.rowCachedMin ? d
            : this.rowCachedMin;
        }
      }
      if (this.rowCachedMin === this.rowCachedMax) {
        this.rowCachedMin--;
      }
      return true;
    }
    return false;
  }
};

phantasus.HeatMapSynchronizer = function () {
  this.controllers = [];
};
phantasus.HeatMapSynchronizer.prototype = {
  firing: false,
  getProject: function () {
    return this.controllers[0].getProject();
  },
  zoom: function () {
    this.controllers[0].zoom.apply(this.controllers[0], arguments);
  },
  setTrackVisible: function () {
    this.controllers[0].setTrackVisible.apply(this.controllers[0],
      arguments);
  },
  revalidate: function () {
    this.controllers[0].revalidate.apply(this.controllers[0], arguments);
  },
  add: function (heatMap) {
    var that = this;
    this.controllers.push(heatMap);
    // setQuickSearchField, setTrackVisible, removeTrack, updateDataset, zoom, moveTrack, resizeTrack, paintAll, fitToWindow, revalidate, setToolTip, setMousePosition
    heatMap.on('change', function (event) {
      if (!that.firing) {
        var source = event.source;
        var method = event.name;
        that.firing = true;
        _.each(that.controllers, function (c) {
          if (c !== source) {
            c[method].apply(c, event.arguments);
          }
        });
        that.firing = false;
      }
    });
  }
};

phantasus.HeatMapElementCanvas = function (project) {
  phantasus.AbstractCanvas.call(this, true);
  var _this = this;
  this.colorScheme = null;
  this.project = project;
  this.dataset = null;
  this.columnPositions = new phantasus.Positions();
  this.rowPositions = new phantasus.Positions();
  this.lastPosition = {
    left: -1,
    right: -1,
    top: -1,
    bottom: -1
  };
  // drag to select rows and columns
  this.selectionBox = null;
  this.selectedRowElements = [];
  this.selectedColumnElements = [];
  project.getElementSelectionModel().on('selectionChanged', function (e) {
    _this.repaint();
  });
  this.gridColor = phantasus.HeatMapElementCanvas.GRID_COLOR;
  this.gridThickness = 0.1;
  this.elementDrawCallback = null;
  this.drawCallback = null;
  this.drawValuesFormat = phantasus.Util.createNumberFormat('.5g');
};
phantasus.HeatMapElementCanvas.GRID_COLOR = '#808080';
phantasus.HeatMapElementCanvas.prototype = {
  drawGrid: true,
  drawValues: false,
  setPropertiesFromParent: function (parentHeatMapElementCanvas) {
    this.drawGrid = parentHeatMapElementCanvas.drawGrid;
    this.gridThickness = parentHeatMapElementCanvas.gridThickness;
    this.gridColor = parentHeatMapElementCanvas.gridColor;
    this.drawValues = parentHeatMapElementCanvas.drawValues;
  },
  updateRowSelectionCache: function (repaint) {
    this.selectedRowElements = phantasus.HeatMapElementCanvas.getSelectedSpans(this.project.getRowSelectionModel().getViewIndices());
    if (repaint) {
      this.repaint();
    }
  },
  updateColumnSelectionCache: function (repaint) {
    this.selectedColumnElements = phantasus.HeatMapElementCanvas.getSelectedSpans(this.project.getColumnSelectionModel().getViewIndices());
    if (repaint) {
      this.repaint();
    }
  },
  setGridColor: function (gridColor) {
    this.gridColor = gridColor;
  },
  getGridColor: function () {
    return this.gridColor;
  },
  setGridThickness: function (gridThickness) {
    this.gridThickness = gridThickness;
  },
  getGridThickness: function () {
    return this.gridThickness;
  },
  getColorScheme: function () {
    return this.colorScheme;
  },
  isDrawGrid: function () {
    return this.drawGrid;
  },
  setDrawGrid: function (drawGrid) {
    this.drawGrid = drawGrid;
  },
  getDrawValuesFormat: function () {
    return this.drawValuesFormat;
  },
  setDrawValuesFormat: function (f) {
    if (typeof f === 'object') { // convert to function
      f = phantasus.Util.createNumberFormat(f.pattern);
    }
    this.drawValuesFormat = f;
  },
  setDrawValues: function (drawValues) {
    this.drawValues = drawValues;
  },
  isDrawValues: function () {
    return this.drawValues;
  },
  setColorScheme: function (colorScheme) {
    this.colorScheme = colorScheme;
  },
  setDataset: function (dataset) {
    this.dataset = dataset;
    this.columnPositions.setLength(this.dataset.getColumnCount());
    this.rowPositions.setLength(this.dataset.getRowCount());
    this.updateRowSelectionCache(false);
    this.updateColumnSelectionCache(false);
  },
  getColumnPositions: function () {
    return this.columnPositions;
  },
  getRowPositions: function () {
    return this.rowPositions;
  },
  getPreferredSize: function (context) {
    var w = Math.ceil(this.columnPositions.getPosition(this.columnPositions
          .getLength() - 1)
      + this.columnPositions.getItemSize(this.columnPositions
          .getLength() - 1));
    var h = Math.ceil(this.rowPositions.getPosition(this.rowPositions
          .getLength() - 1)
      + this.rowPositions
        .getItemSize(this.rowPositions.getLength() - 1));
    return {
      width: w,
      height: h
    };
  },
  prePaint: function (clip, context) {
    var lastPosition = this.lastPosition;
    var columnPositions = this.columnPositions;
    var rowPositions = this.rowPositions;
    var left = phantasus.Positions.getLeft(clip, columnPositions);
    var right = phantasus.Positions.getRight(clip, columnPositions);
    var top = phantasus.Positions.getTop(clip, rowPositions);
    var bottom = phantasus.Positions.getBottom(clip, rowPositions);
    if (this.invalid || left !== lastPosition.left
      || right !== lastPosition.right || top !== lastPosition.top
      || bottom !== lastPosition.bottom) {
      lastPosition.right = right;
      lastPosition.left = left;
      lastPosition.top = top;
      lastPosition.bottom = bottom;
      this.invalid = true;
    }
  },
  postPaint: function (clip, context) {
    // draw mouse over stuff
    phantasus.CanvasUtil.resetTransform(context);
    var project = this.project;
    context.strokeStyle = 'Grey';
    context.lineWidth = 1;
    var rowPositions = this.getRowPositions();
    var columnPositions = this.getColumnPositions();
    if (project.getHoverColumnIndex() >= 0
      || project.getHoverRowIndex() >= 0) {
      var height = rowPositions
        .getItemSize(project.getHoverColumnIndex());
      var width = columnPositions.getItemSize(project
        .getHoverColumnIndex());
      var y = (project.getHoverRowIndex() === -1 ? rowPositions
        .getPosition(rowPositions.getLength() - 1) : rowPositions
        .getPosition(project.getHoverRowIndex()));
      var x = (project.getHoverColumnIndex() === -1 ? columnPositions
        .getPosition(0) : columnPositions.getPosition(project
        .getHoverColumnIndex()));

      if (project.getHoverColumnIndex() !== -1) {
        // thin rectangle down entire column
        context.strokeRect(x - clip.x, 0, width, this
          .getUnscaledHeight());
      }
      if (project.getHoverRowIndex() !== -1) {
        // thin rectangle across entire row
        context.strokeRect(0, y - clip.y, this.getUnscaledWidth(),
          height);
      }
      if (project.getHoverColumnIndex() !== -1
        && project.getHoverRowIndex() !== -1) {
        context.strokeStyle = 'black';
        context.lineWidth = 3;
        context.strokeRect(x - clip.x + 1.5, y - clip.y + 1.5,
          width - 1.5, height - 1.5);
        if (project.isSymmetric()) {
          var y2 = rowPositions.getPosition(project.getHoverColumnIndex());
          var x2 = columnPositions.getPosition(project.getHoverRowIndex());
          context.strokeRect(x2 - clip.x + 1.5, y2 - clip.y + 1.5,
            width - 1.5, height - 1.5);
        }

      }
    }
    var left = phantasus.Positions.getLeft(clip, columnPositions);
    var right = phantasus.Positions.getRight(clip, columnPositions);
    var top = phantasus.Positions.getTop(clip, rowPositions);
    var bottom = phantasus.Positions.getBottom(clip, rowPositions);

    context.strokeStyle = 'rgb(0,0,0)';
    context.lineWidth = 2;
    // context.fillRect(0, 0, this.canvas.width, this.canvas.height);
    context.translate(-clip.x, -clip.y);
    var selectedElements = project.getElementSelectionModel()
      .getViewIndices();

    if (selectedElements != null) {
      selectedElements.forEach(function (id) {
        var rowIndex = id.getArray()[0];
        var columnIndex = id.getArray()[1];
        if (rowIndex >= top && rowIndex < bottom && columnIndex >= left
          && columnIndex < right) {
          var rowSize = rowPositions.getItemSize(rowIndex);
          var py = rowPositions.getPosition(rowIndex);
          var columnSize = columnPositions.getItemSize(columnIndex);
          var px = columnPositions.getPosition(columnIndex);
          context.strokeRect(px + 1.5, py + 1.5, columnSize - 1.5,
            rowSize - 1.5);

        }
      });
    }
    // draw selection bounding boxes
    // context.strokeStyle = 'rgb(182,213,253)';
    context.strokeStyle = 'rgb(60,60,60)';
    var selectedRowElements = this.selectedRowElements;
    var selectedColumnElements = this.selectedColumnElements;

    if (!(selectedRowElements.length === 0 &&
        selectedColumnElements.length === 0)) {
      if (selectedRowElements.length === 0) {
        selectedRowElements = [[top, bottom - 1]];
      }
      if (selectedColumnElements.length === 0) {
        selectedColumnElements = [[left, right - 1]];
      }
    }
    var nrows = selectedRowElements.length;
    var ncols = selectedColumnElements.length;

    if (nrows !== 0 || ncols !== 0) {
      for (var i = 0; i < nrows; i++) {
        var r = selectedRowElements[i];
        var y1 = rowPositions.getPosition(r[0]);
        var y2 = rowPositions.getPosition(r[1]) + rowPositions.getItemSize(i);
        for (var j = 0; j < ncols; j++) {
          var c = selectedColumnElements[j];
          var x1 = columnPositions.getPosition(c[0]);
          var x2 = columnPositions.getPosition(c[1]) + columnPositions.getItemSize(j);
          context.strokeRect(x1, y1, x2 - x1, y2 - y1);
        }
      }
    }
    if (this.selectionBox) {
      context.strokeStyle = 'rgb(0,0,0)';
      context.lineWidth = 2;
      if (context.setLineDash) {
        context.setLineDash([5]);
      }
      var x1 = columnPositions.getPosition(this.selectionBox.x[0]);
      var x2 = columnPositions.getPosition(this.selectionBox.x[1]);
      if (x2 < x1) {
        var tmp = x1;
        x1 = x2;
        x2 = tmp + columnPositions.getItemSize(this.selectionBox.x[0]);
      } else {
        x2 += columnPositions.getItemSize(this.selectionBox.x[1]);
      }
      var y1 = rowPositions.getPosition(this.selectionBox.y[0]);
      var y2 = rowPositions.getPosition(this.selectionBox.y[1]);
      if (y2 < y1) {
        var tmp = y1;
        y1 = y2;
        y2 = tmp + rowPositions.getItemSize(this.selectionBox.y[0]);
      } else {
        y2 += rowPositions.getItemSize(this.selectionBox.y[1]);
      }

      context.strokeRect(x1, y1, x2 - x1, y2 - y1);
      if (context.setLineDash) {
        context.setLineDash([]);
      }
      context.lineWidth = 1;
    }
  },
  setElementDrawCallback: function (elementDrawCallback) {
    this.elementDrawCallback = elementDrawCallback;
  },
  setSelectionBox: function (selectionBox) {
    this.selectionBox = selectionBox;
  },
  setDrawCallback: function (drawCallback) {
    this.drawCallback = drawCallback;
  },
  draw: function (clip, context) {
    var columnPositions = this.columnPositions;
    var rowPositions = this.rowPositions;
    var left = phantasus.Positions.getLeft(clip, columnPositions);
    var right = phantasus.Positions.getRight(clip, columnPositions);
    var top = phantasus.Positions.getTop(clip, rowPositions);
    var bottom = phantasus.Positions.getBottom(clip, rowPositions);

      context.translate(-clip.x, -clip.y);
      this._draw({
        left: left,
        right: right,
        top: top,
        bottom: bottom,
        context: context
      });
      context.translate(clip.x, clip.y);

    if (this.drawCallback) {
      this.drawCallback({
        clip: clip,
        context: context
      });
    }

  },
  _draw: function (options) {
    var left = options.left;
    var right = options.right;
    var top = options.top;
    var bottom = options.bottom;
    var context = options.context;
    var fontFamily = phantasus.CanvasUtil.getFontFamily(context);
    var columnPositions = this.columnPositions;
    var rowPositions = this.rowPositions;
    //if (rowPositions.getSize() < 1 || columnPositions.getSize() < 1) {
    //force sub-pixel rendering
    phantasus.CanvasUtil.forceSubPixelRendering(context);
    //}

    context.textAlign = 'center';
    context.textBaseline = 'middle';
    var dataset = this.dataset;

    var colorScheme = this.colorScheme;
    var drawGrid = this.drawGrid;
    var elementDrawCallback = this.elementDrawCallback;
    var hasElementDrawCallback = elementDrawCallback != null;
    var drawValues = this.drawValues && columnPositions.getSize() > 7 && rowPositions.getSize() > 7;
    var nf;
    if (drawValues) {
      nf = this.drawValuesFormat;
      var fontSize = columnPositions.getSize();
      context.font = fontSize + 'px ' + fontFamily;
      var textWidth = context.measureText('-99.9').width;
      fontSize = ( (columnPositions.getSize() - 1) / textWidth) * fontSize;
      fontSize = Math.min(fontSize, 17);
      context.font = fontSize + 'px ' + phantasus.CanvasUtil.getFontFamily(context);
    }
    var seriesNameToIndex = {};
    for (var i = 0; i < dataset.getSeriesCount(); i++) {
      seriesNameToIndex[dataset.getName(i)] = i;
    }
    var sizer;
    var sizeBySeriesName;
    var sizeBySeriesIndex;

    var conditions;
    var conditionSeriesIndices;
    var sizeFractionRemapper = d3.scale.linear().domain([0, 1]).range([0.2, 1]);
    for (var row = top; row < bottom; row++) {
      var rowSize = rowPositions.getItemSize(row);
      var py = rowPositions.getPosition(row);
      for (var column = left; column < right; column++) {
        var columnSize = columnPositions.getItemSize(column);
        var px = columnPositions.getPosition(column);
        var value = dataset.getValue(row, column);
        context.fillStyle = colorScheme.getColor(row, column, value);
        if (column === left) { // check if the color scheme for this
          // row is sizing
          sizer = colorScheme.getSizer();
          sizeBySeriesName = sizer.getSeriesName();
          sizeBySeriesIndex = sizeBySeriesName != null ? seriesNameToIndex[sizeBySeriesName]
            : undefined;
          conditionSeriesIndices = [];
          conditions = colorScheme.getConditions().getConditions();
          for (var ci = 0, nconditions = conditions.length; ci < nconditions; ci++) {
            conditionSeriesIndices
              .push(seriesNameToIndex[conditions[ci].seriesName]);
          }

        }
        var yoffset = 0;
        var xoffset = 0;
        var cellRowSize = rowSize;
        var cellColumnSize = columnSize;
        if (sizeBySeriesIndex !== undefined) {
          var sizeByValue = dataset.getValue(row, column,
            sizeBySeriesIndex);
          if (!isNaN(sizeByValue)) {
            var sizeFraction = sizeFractionRemapper(sizer.valueToFraction(sizeByValue)); // remap 0-1 to 0.2-1
            cellRowSize = cellRowSize * sizeFraction;
            yoffset = (rowSize - cellRowSize) / 2;

            cellColumnSize = cellColumnSize * sizeFraction;
            xoffset = (columnSize - cellColumnSize) / 2;

          }
        }
        if (conditions.length > 0) {
          var condition = null;
          for (var ci = 0, nconditions = conditions.length; ci < nconditions; ci++) {
            var cond = conditions[ci];
            var condValue = dataset.getValue(row, column,
              conditionSeriesIndices[ci]);

            if (!isNaN(condValue) && cond.accept(condValue)) {
              condition = cond;
              break;
            }

          }
          if (condition !== null) {
            if (condition.shape != null) {
              if (condition.inheritColor) {
                if (sizeBySeriesIndex === undefined) {
                  xoffset = 1;
                  yoffset = 1;
                  cellRowSize -= 2;
                  cellColumnSize -= 2;
                }
                var x = px + xoffset + cellRowSize / 2;
                var y = py + yoffset + cellColumnSize / 2;
                phantasus.CanvasUtil.drawShape(context, condition.shape,
                  x, y, Math.min(cellColumnSize, cellRowSize) / 2, true);
              } else { // e.g. filled circle on top of heat map
                context.fillRect(px + xoffset, py + yoffset, cellColumnSize,
                  cellRowSize);
                // x and y are at center
                var x = px + xoffset + cellRowSize / 2;
                var y = py + yoffset + cellColumnSize / 2;
                context.fillStyle = condition.color;
                phantasus.CanvasUtil.drawShape(context, condition.shape,
                  x, y, Math.min(cellColumnSize, cellRowSize) / 4, true);
              }

            } else {
              context.fillRect(px + xoffset, py + yoffset, cellColumnSize,
                cellRowSize);
            }
          } else {
            context.fillRect(px + xoffset, py + yoffset, cellColumnSize,
              cellRowSize);
          }
        } else {
          context.fillRect(px + xoffset, py + yoffset, cellColumnSize, cellRowSize);
        }
        if (drawValues && cellColumnSize > 7 && cellRowSize > 7 && !isNaN(value)) {
          context.fillStyle = 'rgb(0,0,0)';
          context.fillText(nf(value), px + xoffset + cellColumnSize / 2, py + yoffset + cellRowSize / 2, cellColumnSize);
        }
        if (hasElementDrawCallback) {
          elementDrawCallback(context, dataset, row, column, px, py,
            columnSize, rowSize);
        }
      }
    }
    if (drawGrid && rowPositions.getSize() > 10 && columnPositions.getSize() > 10) {
      context.strokeStyle = this.gridColor;
      context.lineWidth = this.gridThickness;
      context.beginPath();

      for (var row = top; row < bottom; row++) {
        var rowSize = rowPositions.getItemSize(row);
        var py = rowPositions.getPosition(row);
        for (var column = left; column < right; column++) {
          var columnSize = columnPositions.getItemSize(column);
          var px = columnPositions.getPosition(column);
          var grid = columnSize > 10 && rowSize > 10;
          if (grid) {
            context.rect(px, py, columnSize, rowSize);
          }
        }
      }
      context.stroke();

    }
    context.lineWidth = 1;
  }
};
phantasus.Util.extend(phantasus.HeatMapElementCanvas, phantasus.AbstractCanvas);

phantasus.HeatMapElementCanvas.getSelectedSpans = function (set) {
  var array = [];
  if (set.size() > 0) {
    var index = 0;
    var start = index;
    var viewIndices = set.values();
    viewIndices.sort(function (a, b) {
      return (a === b ? 0 : (a < b ? -1 : 1));
    });
    var length = viewIndices.length;
    while (index < length) {
      var prior = index === 0 ? viewIndices[0] : viewIndices[index - 1];
      var current = viewIndices[index];
      if ((current - prior) > 1) {
        array.push([viewIndices[start], viewIndices[index - 1]]);
        start = index;
      }
      index++;
    }
    if (start == 0) {
      array.push([viewIndices[0], viewIndices[viewIndices.length - 1]]);
    } else {
      array.push([viewIndices[start], viewIndices[index - 1]]);
    }
  }
  return array;
};

phantasus.KeyboardCharMap = [
  '', // [0]
  '', // [1]
  '', // [2]
  'CANCEL', // [3]
  '', // [4]
  '', // [5]
  'HELP', // [6]
  '', // [7]
  'BACKSPACE', // [8]
  'TAB', // [9]
  '', // [10]
  '', // [11]
  'CLEAR', // [12]
  'ENTER', // [13]
  'ENTER_SPECIAL', // [14]
  '', // [15]
  'SHIFT', // [16]
  'CONTROL', // [17]
  'ALT', // [18]
  'PAUSE', // [19]
  'CAPS_LOCK', // [20]
  'KANA', // [21]
  'EISU', // [22]
  'JUNJA', // [23]
  'FINAL', // [24]
  'HANJA', // [25]
  '', // [26]
  'Escape', // [27]
  'CONVERT', // [28]
  'NONCONVERT', // [29]
  'ACCEPT', // [30]
  'MODECHANGE', // [31]
  'Space', // [32]
  'Page Up', // [33]
  'Page Down', // [34]
  'End', // [35]
  'Home', // [36]
  'Left', // [37]
  'Up', // [38]
  'Right', // [39]
  'Down', // [40]
  'SELECT', // [41]
  'PRINT', // [42]
  'EXECUTE', // [43]
  'PRINTSCREEN', // [44]
  'INSERT', // [45]
  'Delete', // [46]
  '', // [47]
  '0', // [48]
  '1', // [49]
  '2', // [50]
  '3', // [51]
  '4', // [52]
  '5', // [53]
  '6', // [54]
  '7', // [55]
  '8', // [56]
  '9', // [57]
  'COLON', // [58]
  'SEMICOLON', // [59]
  'LESS_THAN', // [60]
  'Equals', // [61]
  'GREATER_THAN', // [62]
  'QUESTION_MARK', // [63]
  'AT', // [64]
  'A', // [65]
  'B', // [66]
  'C', // [67]
  'D', // [68]
  'E', // [69]
  'F', // [70]
  'G', // [71]
  'H', // [72]
  'I', // [73]
  'J', // [74]
  'K', // [75]
  'L', // [76]
  'M', // [77]
  'N', // [78]
  'O', // [79]
  'P', // [80]
  'Q', // [81]
  'R', // [82]
  'S', // [83]
  'T', // [84]
  'U', // [85]
  'V', // [86]
  'W', // [87]
  'X', // [88]
  'Y', // [89]
  'Z', // [90]
  'OS_KEY', // [91] Windows Key (Windows) or Command Key (Mac)
  '', // [92]
  'CONTEXT_MENU', // [93]
  '', // [94]
  'SLEEP', // [95]
  '0', // [96]
  '1', // [97]
  '2', // [98]
  '3', // [99]
  '4', // [100]
  '5', // [101]
  '6', // [102]
  '7', // [103]
  '8', // [104]
  '9', // [105]
  'MULTIPLY', // [106]
  '+', // [107]
  'SEPARATOR', // [108]
  'SUBTRACT', // [109]
  'DECIMAL', // [110]
  'DIVIDE', // [111]
  'F1', // [112]
  'F2', // [113]
  'F3', // [114]
  'F4', // [115]
  'F5', // [116]
  'F6', // [117]
  'F7', // [118]
  'F8', // [119]
  'F9', // [120]
  'F10', // [121]
  'F11', // [122]
  'F12', // [123]
  'F13', // [124]
  'F14', // [125]
  'F15', // [126]
  'F16', // [127]
  'F17', // [128]
  'F18', // [129]
  'F19', // [130]
  'F20', // [131]
  'F21', // [132]
  'F22', // [133]
  'F23', // [134]
  'F24', // [135]
  '', // [136]
  '', // [137]
  '', // [138]
  '', // [139]
  '', // [140]
  '', // [141]
  '', // [142]
  '', // [143]
  'NUM_LOCK', // [144]
  'SCROLL_LOCK', // [145]
  'WIN_OEM_FJ_JISHO', // [146]
  'WIN_OEM_FJ_MASSHOU', // [147]
  'WIN_OEM_FJ_TOUROKU', // [148]
  'WIN_OEM_FJ_LOYA', // [149]
  'WIN_OEM_FJ_ROYA', // [150]
  '', // [151]
  '', // [152]
  '', // [153]
  '', // [154]
  '', // [155]
  '', // [156]
  '', // [157]
  '', // [158]
  '', // [159]
  'CIRCUMFLEX', // [160]
  'EXCLAMATION', // [161]
  'DOUBLE_QUOTE', // [162]
  'HASH', // [163]
  'DOLLAR', // [164]
  'PERCENT', // [165]
  'AMPERSAND', // [166]
  'UNDERSCORE', // [167]
  'OPEN_PAREN', // [168]
  'CLOSE_PAREN', // [169]
  'ASTERISK', // [170]
  'Plus', // [171]
  'PIPE', // [172]
  '-', // [173]
  'OPEN_CURLY_BRACKET', // [174]
  'CLOSE_CURLY_BRACKET', // [175]
  'TILDE', // [176]
  '', // [177]
  '', // [178]
  '', // [179]
  '', // [180]
  'VOLUME_MUTE', // [181]
  'VOLUME_DOWN', // [182]
  'VOLUME_UP', // [183]
  '', // [184]
  '', // [185]
  'SEMICOLON', // [186]
  'EQUALS', // [187]
  'COMMA', // [188]
  'MINUS', // [189]
  'PERIOD', // [190]
  '/', // [191]
  'BACK_QUOTE', // [192]
  '', // [193]
  '', // [194]
  '', // [195]
  '', // [196]
  '', // [197]
  '', // [198]
  '', // [199]
  '', // [200]
  '', // [201]
  '', // [202]
  '', // [203]
  '', // [204]
  '', // [205]
  '', // [206]
  '', // [207]
  '', // [208]
  '', // [209]
  '', // [210]
  '', // [211]
  '', // [212]
  '', // [213]
  '', // [214]
  '', // [215]
  '', // [216]
  '', // [217]
  '', // [218]
  'OPEN_BRACKET', // [219]
  'BACK_SLASH', // [220]
  'CLOSE_BRACKET', // [221]
  'QUOTE', // [222]
  '', // [223]
  'META', // [224]
  'ALTGR', // [225]
  '', // [226]
  'WIN_ICO_HELP', // [227]
  'WIN_ICO_00', // [228]
  '', // [229]
  'WIN_ICO_CLEAR', // [230]
  '', // [231]
  '', // [232]
  'WIN_OEM_RESET', // [233]
  'WIN_OEM_JUMP', // [234]
  'WIN_OEM_PA1', // [235]
  'WIN_OEM_PA2', // [236]
  'WIN_OEM_PA3', // [237]
  'WIN_OEM_WSCTRL', // [238]
  'WIN_OEM_CUSEL', // [239]
  'WIN_OEM_ATTN', // [240]
  'WIN_OEM_FINISH', // [241]
  'WIN_OEM_COPY', // [242]
  'WIN_OEM_AUTO', // [243]
  'WIN_OEM_ENLW', // [244]
  'WIN_OEM_BACKTAB', // [245]
  'ATTN', // [246]
  'CRSEL', // [247]
  'EXSEL', // [248]
  'EREOF', // [249]
  'PLAY', // [250]
  'ZOOM', // [251]
  '', // [252]
  'PA1', // [253]
  'WIN_OEM_CLEAR', // [254]
  '' // [255]
];
phantasus.HeatMapKeyListener = function (heatMap) {
  var allActions = heatMap.getActionManager().getActions();
  var actions = allActions.filter(function (a) {
    return a.cb != null && a.which != null;
  });
  allActions.sort(function (a, b) {
    a = a.name.toLowerCase();
    b = b.name.toLowerCase();
    return (a === b ? 0 : (a < b ? -1 : 1));
  });
  var keydown = function (e) {
    var tagName = e.target.tagName;
    var found = false;
    var commandKey = phantasus.Util.IS_MAC ? e.metaKey : e.ctrlKey;
    var altKey = e.altKey;
    var shiftKey = e.shiftKey;
    var which = e.which;
    var isInputField = (tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA');
    var acceptOptions = {
      isInputField: isInputField,
      heatMap: heatMap
    };
    var shortcutMatches = function (sc) {
      if (sc.which.indexOf(which) !== -1 && (sc.commandKey === undefined || commandKey === sc.commandKey) && (sc.shiftKey === undefined || shiftKey === sc.shiftKey) &&
        (sc.accept == undefined || sc.accept(acceptOptions))) {
        sc.cb({heatMap: heatMap});
        return true;
      }
    };

    if (!isInputField) {
      for (var i = 0, n = actions.length; i < n; i++) {
        var sc = actions[i];
        if (shortcutMatches(sc)) {
          found = true;
          break;
        }
      }
    } else { // only search global shortcuts
      for (var i = 0, n = actions.length; i < n; i++) {
        var sc = actions[i];
        if (sc.global && shortcutMatches(sc)) {
          found = true;
          break;
        }
      }
    }

    if (found) {
      e.preventDefault();
      e.stopPropagation();
      e.stopImmediatePropagation();
      return false;
    }
  };
  var $keyelement = heatMap.$tabPanel;
  $keyelement.on('keydown', keydown);

  $keyelement.on('dragover.phantasus dragenter.phantasus', function (e) {
    e.preventDefault();
    e.stopPropagation();
  }).on(
    'drop.phantasus',
    function (e) {
      if (heatMap.options.menu.File && heatMap.options.menu.File.indexOf('Open') !== -1 && e.originalEvent.dataTransfer
        && e.originalEvent.dataTransfer.files.length) {
        e.preventDefault();
        e.stopPropagation();
        var files = e.originalEvent.dataTransfer.files;
        for (var i = 0; i < files.length; i++) {
          phantasus.HeatMap.showTool(new phantasus.OpenFileTool({
            file: files[i]
          }), heatMap);
        }

      }
    });
  $keyelement.on('paste.phantasus',
    function (e) {
      if (heatMap.options.toolbar.openFile) {
        var tagName = e.target.tagName;
        if (tagName == 'INPUT' || tagName == 'SELECT'
          || tagName == 'TEXTAREA') {
          return;
        }
        var text = e.originalEvent.clipboardData.getData('text/plain');
        if (text != null && text.length > 0) {
          e.preventDefault();
          e.stopPropagation();
          var blob = new Blob([text]);
          var url = window.URL.createObjectURL(blob);
          phantasus.HeatMap.showTool(new phantasus.OpenFileTool({
            file: url
          }), heatMap);
        }
      }
    });

  $keyelement.on('mousewheel', function (e) {
    var scrolly = e.deltaY * e.deltaFactor;
    var scrollx = e.deltaX * e.deltaFactor;
    var stop = false;
    if (e.altKey) {
      heatMap.zoom(scrolly > 0, {
        rows: true,
        columns: true
      });
      stop = true;
    } else {
      if (scrolly !== 0) {
        var scrollTop = heatMap.scrollTop();
        if (heatMap.scrollTop(scrollTop - scrolly) !== scrollTop) {
          stop = true;
        }
      }
      if (scrollx !== 0) {
        var scrollLeft = heatMap.scrollLeft();
        if (heatMap.scrollLeft(scrollLeft + scrollx) !== scrollLeft) {
          stop = true;
        }
      }
    }
    if (stop && heatMap.options.standalone) {
      e.preventDefault();
      e.stopPropagation();
    }
  });

  function shortcutToString(sc) {
    var s = ['<b>'];

    if (sc.commandKey) {
      s.push(phantasus.Util.COMMAND_KEY);
    }
    if (sc.shiftKey) {
      s.push('Shift+');
    }
    if (sc.which) {
      s.push(phantasus.KeyboardCharMap[sc.which[0]]);
    }
    s.push('</b>');
    return s.join('');
  }

  this.showKeyMapReference = function () {
    var html = [];
    html.push('<table class="table table-condensed">');
    allActions.forEach(function (sc) {
      html.push('<tr><td>');
      html.push(shortcutToString(sc));
      html.push('</td><td>');
      if (sc.icon) {
        html.push('<span class="' + sc.icon + '"></span> ');
      }

      html.push(sc.name);
      html.push('</td></tr>');
    });

    html.push('</table>');
    phantasus.FormBuilder.showInModal({
      title: 'Keymap Shortcuts',
      html: html.join(''),
      focus: document.activeElement
    });
  };
};

phantasus.HeatMapOptions = function (heatMap) {
  var items = [
    {
      name: 'color_by',
      required: true,
      help: 'Use a different color scheme for distinct row annotation values',
      type: 'select',
      options: ['(None)'].concat(phantasus.MetadataUtil
        .getMetadataNames(heatMap.getProject()
          .getFullDataset().getRowMetadata())),
      value: heatMap.heatmap.getColorScheme()
        .getSeparateColorSchemeForRowMetadataField()
    }, {
      name: 'color_by_value',
      required: true,
      type: 'select',
      options: []
    }];

  items.push({
    name: 'size_by',
    required: true,
    type: 'select',
    options: ['(None)'].concat(phantasus.DatasetUtil
      .getSeriesNames(heatMap.getProject().getFullDataset()))
  });
  items.push({
    name: 'size_by_minimum',
    title: 'Size by minimum',
    required: true,
    type: 'text',
    style: 'max-width: 100px;'
  });
  items.push({
    name: 'size_by_maximum',
    title: 'Size by maximum',
    required: true,
    type: 'text',
    style: 'max-width: 100px;'
  });

  items.push({
    name: 'conditional_rendering',
    required: true,
    type: 'button'
  });

  items.push({type: 'separator'});

  var createColorSchemeOptions = function () {
    var colorSchemeOptions = [
      {
        name: 'relative',
        value: 'relative'
      }, {
        name: 'binary',
        value: 'binary'
      }, {
        name: 'MAF',
        value: 'MAF'
      }, {
        name: 'fixed (-1.5, -0.1, 0.1, 1.5)',
        value: 'cn'
      }];
    var savedColorSchemeKeys = [];
    if (localStorage.getItem('phantasus-colorScheme') != null) {
      savedColorSchemeKeys = _.keys(JSON.parse(localStorage.getItem('phantasus-colorScheme')));
    }
    if (savedColorSchemeKeys.length > 0) {
      colorSchemeOptions.push({divider: true});
      colorSchemeOptions = colorSchemeOptions.concat(savedColorSchemeKeys);
    }
    colorSchemeOptions.push({divider: true});
    colorSchemeOptions.push('My Computer...');
    return colorSchemeOptions;
  };

  items.push([
    {
      name: 'saved_color_scheme',
      required: true,
      type: 'bootstrap-select',
      options: createColorSchemeOptions()
    }, {name: 'load_color_scheme', type: 'button'}, {name: 'delete_color_scheme', type: 'button'}]);
  items.push({
    name: 'save_color_scheme',
    type: 'button'
  });

  var displayItems = [
    {
      disabled: heatMap.getProject().getFullDataset().getColumnCount() !== heatMap.getProject().getFullDataset().getRowCount(),
      name: 'link_rows_and_columns',
      help: 'For square matrices',
      required: true,
      type: 'checkbox',
      style: 'max-width: 100px;',
      value: heatMap.getProject().isSymmetric()
    },
    {
      name: 'show_row_number',
      required: true,
      type: 'checkbox',
      value: heatMap.isShowRowNumber()
    },
    {
      name: 'show_grid',
      required: true,
      type: 'checkbox',
      value: heatMap.heatmap.isDrawGrid()
    },
    {
      name: 'grid_thickness',
      required: true,
      type: 'text',
      style: 'max-width: 100px;',
      value: phantasus.Util.nf(heatMap.heatmap.getGridThickness())
    },
    {
      name: 'grid_color',
      required: true,
      type: 'color',
      style: 'max-width: 50px;',
      value: heatMap.heatmap.getGridColor()
    },
    {
      name: 'row_size',
      required: true,
      type: 'text',
      style: 'max-width: 100px;',
      value: phantasus.Util.nf(heatMap.heatmap.getRowPositions()
        .getSize())
    },
    {
      name: 'column_size',
      required: true,
      type: 'text',
      style: 'max-width: 100px;',
      value: phantasus.Util.nf(heatMap.heatmap
        .getColumnPositions().getSize())
    }, {
      name: 'show_values',
      required: true,
      type: 'checkbox',
      value: heatMap.heatmap.isDrawValues()
    }, {
      name: 'number_of_fraction_digits',
      required: true,
      type: 'number',
      min: 0,
      step: 1,
      style: 'max-width: 100px;',
      value: phantasus.Util.getNumberFormatPatternFractionDigits(heatMap.heatmap.getDrawValuesFormat().toJSON().pattern)
    }];
  if (heatMap.rowDendrogram) {
    displayItems
      .push({
        name: 'row_dendrogram_line_thickness',
        required: true,
        type: 'text',
        style: 'max-width: 100px;',
        value: phantasus.Util
          .nf(heatMap.rowDendrogram ? heatMap.rowDendrogram.lineWidth
            : 1)
      });
  }
  if (heatMap.columnDendrogram) {
    displayItems
      .push({
        name: 'column_dendrogram_line_thickness',
        required: true,
        type: 'text',
        style: 'max-width: 100px;',
        value: phantasus.Util
          .nf(heatMap.columnDendrogram ? heatMap.columnDendrogram.lineWidth
            : 1)
      });
  }

  displayItems.push({
    name: 'info_window',
    required: true,
    type: 'select',
    style: 'max-width:130px;',
    options: [
      {
        name: 'Fixed To Top',
        value: 0
      }, {
        name: 'New Window',
        value: 1
      }],
    value: heatMap.tooltipMode
  });

  displayItems.push({
    name: 'inline_tooltip',
    required: true,
    type: 'checkbox',
    value: heatMap.options.inlineTooltip
  });

  var colorSchemeFormBuilder = new phantasus.FormBuilder();
  _.each(items, function (item) {
    colorSchemeFormBuilder.append(item);
  });
  var displayFormBuilder = new phantasus.FormBuilder();
  _.each(displayItems, function (item) {
    displayFormBuilder.append(item);
  });
  var colorSchemeChooser = new phantasus.HeatMapColorSchemeChooser({
    showRelative: true,
    colorScheme: heatMap.heatmap
      .getColorScheme()
  });
  var updatingSizer = false;

  function colorSchemeChooserUpdated() {
    if (heatMap.heatmap.getColorScheme().getSizer
      && heatMap.heatmap.getColorScheme().getSizer() != null) {
      colorSchemeFormBuilder.setValue('size_by', heatMap.heatmap
        .getColorScheme().getSizer().getSeriesName());
      colorSchemeFormBuilder.setEnabled('size_by_minimum',
        heatMap.heatmap.getColorScheme().getSizer()
          .getSeriesName() != null);
      colorSchemeFormBuilder.setEnabled('size_by_maximum',
        heatMap.heatmap.getColorScheme().getSizer()
          .getSeriesName() != null);

      if (!updatingSizer) {
        colorSchemeFormBuilder.setValue('size_by_minimum',
          heatMap.heatmap.getColorScheme().getSizer().getMin());
        colorSchemeFormBuilder.setValue('size_by_maximum',
          heatMap.heatmap.getColorScheme().getSizer().getMax());
      }
    }
  }

  colorSchemeChooser.on('change', function () {
    colorSchemeChooserUpdated();
    // repaint the heat map when color scheme changes
    heatMap.heatmap.setInvalid(true);
    heatMap.heatmap.repaint();
    colorSchemeChooser.restoreCurrentValue();
  });

  function createMetadataField(isColumns) {
    var options = [];
    var value = {};
    _.each(heatMap.getVisibleTrackNames(isColumns), function (name) {
      value[name] = true;
    });
    _.each(phantasus.MetadataUtil.getMetadataNames(isColumns ? heatMap
        .getProject().getFullDataset().getColumnMetadata() : heatMap
        .getProject().getFullDataset().getRowMetadata()),
      function (name) {
        options.push(name);
      });
    var field = {
      type: 'bootstrap-select',
      search: options.length > 10,
      name: isColumns ? 'column_annotations' : 'row_annotations',
      multiple: true,
      value: value,
      options: options,
      toggle: true
    };

    return field;
  }

  var annotationsBuilder = new phantasus.FormBuilder();
  annotationsBuilder.append(createMetadataField(false));
  annotationsBuilder.append(createMetadataField(true));

  function annotationsListener($select, isColumns) {
    var names = [];
    _.each(heatMap.getVisibleTrackNames(isColumns), function (name) {
      names.push(name);
    });
    var values = $select.val();
    var selectedNow = _.difference(values, names);
    var unselectedNow = _.difference(names, values);
    var tracks = [];
    _.each(selectedNow, function (name) {
      tracks.push({
        name: name,
        isColumns: isColumns,
        visible: true
      });
    });
    _.each(unselectedNow, function (name) {
      tracks.push({
        name: name,
        isColumns: isColumns,
        visible: false
      });
    });
    heatMap.setTrackVisibility(tracks);
    colorSchemeChooser.restoreCurrentValue();
  }

  var $ca = annotationsBuilder.$form.find('[name=column_annotations]');
  $ca.on('change', function (e) {
    annotationsListener($(this), true);
  });
  var $ra = annotationsBuilder.$form.find('[name=row_annotations]');
  $ra.on('change', function (e) {
    annotationsListener($(this), false);
  });
  var annotationOptionsTabId = _.uniqueId('phantasus');
  var heatMapOptionsTabId = _.uniqueId('phantasus');
  var displayOptionsTabId = _.uniqueId('phantasus');

  var $metadataDiv = $('<div class="tab-pane" id="' + annotationOptionsTabId
    + '"></div>');
  $metadataDiv.append($(annotationsBuilder.$form));
  var $heatMapDiv = $('<div class="tab-pane active" id="'
    + heatMapOptionsTabId + '"></div>');
  $heatMapDiv.append(colorSchemeChooser.$div);
  $heatMapDiv.append($(colorSchemeFormBuilder.$form));
  var $displayDiv = $('<div class="tab-pane" id="' + displayOptionsTabId
    + '"></div>');
  $displayDiv.append($(displayFormBuilder.$form));
  displayFormBuilder.setEnabled('grid_thickness', heatMap.heatmap.isDrawGrid());
  displayFormBuilder.setEnabled('grid_color', heatMap.heatmap.isDrawGrid());

  displayFormBuilder.$form.find('[name=show_grid]').on('click', function (e) {
    var grid = $(this).prop('checked');
    displayFormBuilder.setEnabled('grid_thickness', grid);
    displayFormBuilder.setEnabled('grid_color', grid);
    heatMap.heatmap.setDrawGrid(grid);
    heatMap.revalidate();
    colorSchemeChooser.restoreCurrentValue();
  });
  var $fractionDigits = displayFormBuilder.$form.find('[name=number_of_fraction_digits]');
  displayFormBuilder.$form.find('[name=show_values]').on('click', function (e) {
    var drawValues = $(this).prop('checked');
    heatMap.heatmap.setDrawValues(drawValues);
    // $fractionDigits.prop('disabled', !drawValues);
    heatMap.revalidate();
    colorSchemeChooser.restoreCurrentValue();
  });

  $fractionDigits.on(
    'keyup input', _.debounce(
      function () {
        var n = parseInt($(this)
          .val());
        if (n >= 0) {
          heatMap.heatmap.setDrawValuesFormat(phantasus.Util.createNumberFormat('.' + n + 'f'));
          heatMap.heatmap.setInvalid(true);
          heatMap.heatmap.repaint();
        }
      }, 100));

  displayFormBuilder.$form.find('[name=inline_tooltip]').on('click',
    function (e) {
      heatMap.options.inlineTooltip = $(this).prop('checked');
    });

  displayFormBuilder.$form.find('[name=grid_color]').on(
    'change',
    function (e) {
      var value = $(this).val();
      heatMap.heatmap.setGridColor(value);
      heatMap.heatmap.setInvalid(true);
      heatMap.heatmap.repaint();
    });

  displayFormBuilder.$form.find('[name=grid_thickness]').on(
    'keyup',
    _.debounce(function (e) {
      var value = parseFloat($(this).val());
      if (!isNaN(value)) {
        heatMap.heatmap.setGridThickness(value);
        heatMap.heatmap.setInvalid(true);
        heatMap.heatmap.repaint();
      }
    }, 100));

  displayFormBuilder.$form.find('[name=row_size]').on(
    'keyup',
    _.debounce(function (e) {
      var value = parseFloat($(this).val());
      if (!isNaN(value)) {
        heatMap.heatmap.getRowPositions().setSize(
          value);
        heatMap.revalidate();
        colorSchemeChooser.restoreCurrentValue();
      }

    }, 100));
  displayFormBuilder.$form.find('[name=info_window]').on('change',
    function (e) {
      heatMap.setTooltipMode(parseInt($(this).val()));
    });
  displayFormBuilder.find('link_rows_and_columns').on('click',
    function (e) {
      var checked = $(this).prop('checked');
      if (checked) {
        heatMap.getProject().setSymmetric(heatMap);
      } else {
        heatMap.getProject().setSymmetric(null);
      }
    });
  displayFormBuilder.find('show_row_number').on('click',
    function (e) {
      var checked = $(this).prop('checked');
      heatMap.setShowRowNumber(checked);
      heatMap.revalidate();
    });

  var $colorByValue = colorSchemeFormBuilder.$form
    .find('[name=color_by_value]');
  var separateSchemesField = heatMap.heatmap.getColorScheme()
    .getSeparateColorSchemeForRowMetadataField();
  if (separateSchemesField != null) {
    var v = heatMap.project.getFullDataset().getRowMetadata()
      .getByName(separateSchemesField);
    if (v != null) {
      $colorByValue.html(phantasus.Util.createOptions(phantasus.VectorUtil
        .createValueToIndexMap(
          v).keys()));
    }
  }

  if (separateSchemesField != null) {
    colorSchemeChooser.setCurrentValue($colorByValue.val());
  }
  if (heatMap.heatmap.getColorScheme().getSizer
    && heatMap.heatmap.getColorScheme().getSizer() != null
    && heatMap.heatmap.getColorScheme().getSizer().getSeriesName()) {
    colorSchemeFormBuilder.setValue('size_by', heatMap.heatmap
      .getColorScheme().getSizer().getSeriesName());
  }
  colorSchemeFormBuilder.$form.find('[name=size_by]')
    .on(
      'change',
      function (e) {
        var series = $(this).val();
        if (series == '(None)') {
          series = null;
        }
        colorSchemeChooser.colorScheme.getSizer()
          .setSeriesName(series);
        colorSchemeChooser.fireChanged();
      });
  colorSchemeFormBuilder.$form.find('[name=size_by_minimum]').on(
    'keyup',
    _.debounce(function (e) {
      updatingSizer = true;
      colorSchemeChooser.colorScheme.getSizer().setMin(
        parseFloat($(this).val()));
      colorSchemeChooser.fireChanged(true);
      updatingSizer = false;
    }, 100));
  colorSchemeFormBuilder.$form.find('[name=size_by_maximum]').on(
    'keyup',
    _.debounce(function (e) {
      updatingSizer = true;
      colorSchemeChooser.colorScheme.getSizer().setMax(
        parseFloat($(this).val()));
      colorSchemeChooser.fireChanged(true);
      updatingSizer = false;
    }, 100));
  colorSchemeFormBuilder.$form
    .find('[name=conditional_rendering]')
    .on(
      'click',
      function (e) {
        e.preventDefault();
        var conditionalRenderingUI = new phantasus.ConditionalRenderingUI(
          heatMap);
        phantasus.FormBuilder.showInModal({
          title: 'Conditional Rendering',
          html: conditionalRenderingUI.$div,
          close: 'Close',
          modalClass: 'phantasus-sub-modal'
        });
      });

  colorSchemeFormBuilder.find('save_color_scheme').on('click', function (e) {
    e.preventDefault();
    // prompt to save to file or local storage
    var saveColorSchemeFormBuilder = new phantasus.FormBuilder();
    saveColorSchemeFormBuilder.append({name: 'save_to', type: 'radio', value: 'Browser Storage', options: ['Browser Storage', 'File']});
    saveColorSchemeFormBuilder.append({name: 'color_scheme_name', type: 'text'});
    saveColorSchemeFormBuilder.append({name: 'file_name', type: 'text'});
    saveColorSchemeFormBuilder.setVisible('file_name', false);
    saveColorSchemeFormBuilder.find('save_to').on('change', function () {
      var isBrowser = $(this).val() === 'Browser Storage';
      saveColorSchemeFormBuilder.setVisible('file_name', !isBrowser);
      saveColorSchemeFormBuilder.setVisible('color_scheme_name', isBrowser);
    });
    phantasus.FormBuilder.showOkCancel({
      title: 'Save Color Scheme',
      ok: true,
      cancel: true,
      draggable: true,
      content: saveColorSchemeFormBuilder.$form,
      appendTo: heatMap.getContentEl(),
      align: 'right',
      okCallback: function () {
        var colorSchemeText = JSON.stringify(heatMap.heatmap.getColorScheme().toJSON());
        if (saveColorSchemeFormBuilder.getValue('save_to') === 'Browser Storage') {
          var name = saveColorSchemeFormBuilder.getValue('color_scheme_name').trim();
          if (name === '') {
            name = 'my color scheme';
          }
          var colorSchemeObject = localStorage.getItem('phantasus-colorScheme');
          if (colorSchemeObject == null) {
            colorSchemeObject = {};
          } else {
            colorSchemeObject = JSON.parse(colorSchemeObject);
          }
          colorSchemeObject[name] = colorSchemeText;
          localStorage.setItem('phantasus-colorScheme', JSON.stringify(colorSchemeObject));
          colorSchemeFormBuilder.setOptions('saved_color_scheme', createColorSchemeOptions());
        } else {
          var name = saveColorSchemeFormBuilder.getValue('file_name').trim();
          if (name === '') {
            name = 'color_scheme.json';
          }
          var blob = new Blob([colorSchemeText], {
            type: 'application/json'
          });
          saveAs(blob, name);
        }
      },
      focus: heatMap.getFocusEl()
    });
  });

  colorSchemeFormBuilder.setEnabled('delete_color_scheme', false);
  colorSchemeFormBuilder.find('delete_color_scheme').on('click', function () {
    var key = colorSchemeFormBuilder.getValue('saved_color_scheme');
    var savedColorSchemes = JSON.parse(localStorage.getItem('phantasus-colorScheme'));
    delete savedColorSchemes[key];
    localStorage.setItem('phantasus-colorScheme', JSON.stringify(savedColorSchemes));
    colorSchemeFormBuilder.setOptions('saved_color_scheme', createColorSchemeOptions());

  });
  colorSchemeFormBuilder.find('saved_color_scheme').on('change', function () {
    colorSchemeFormBuilder.setEnabled('delete_color_scheme', ['relative', 'cn', 'MAF', 'binary', 'My Computer...'].indexOf(
      colorSchemeFormBuilder.getValue('saved_color_scheme')) === -1);
  });
  colorSchemeFormBuilder.find('load_color_scheme').on('click',
    function (e) {
      var val = colorSchemeFormBuilder.getValue('saved_color_scheme');
      var repaint = true;
      if (val === 'relative') {
        heatMap.heatmap
          .getColorScheme()
          .setColorSupplierForCurrentValue(
            phantasus.AbstractColorSupplier.fromJSON(phantasus.HeatMapColorScheme.Predefined
              .RELATIVE()));
      } else if (val === 'cn') {
        heatMap.heatmap
          .getColorScheme()
          .setColorSupplierForCurrentValue(
            phantasus.AbstractColorSupplier.fromJSON(phantasus.HeatMapColorScheme.Predefined
              .CN()));
      } else if (val === 'MAF') {
        heatMap.heatmap
          .getColorScheme()
          .setColorSupplierForCurrentValue(
            phantasus.AbstractColorSupplier.fromJSON(phantasus.HeatMapColorScheme.Predefined
              .MAF()));
      } else if (val === 'binary') {
        heatMap.heatmap
          .getColorScheme()
          .setColorSupplierForCurrentValue(
            phantasus.AbstractColorSupplier.fromJSON(phantasus.HeatMapColorScheme.Predefined
              .BINARY()));
      } else if (val === 'My Computer...') {
        repaint = false;
        var $file = $('<input style="display:none;" type="file">');
        $file.appendTo(heatMap.getContentEl());
        $file.click();
        $file.on('change', function (evt) {
          var files = evt.target.files;
          phantasus.Util.getText(evt.target.files[0]).done(
            function (text) {
              var json = JSON.parse($.trim(text));
              heatMap.heatmap.getColorScheme().fromJSON(json);
              colorSchemeChooser
                .setColorScheme(heatMap.heatmap
                  .getColorScheme());
              heatMap.heatmap.setInvalid(true);
              heatMap.heatmap.repaint();

            }).fail(function () {
            phantasus.FormBuilder.showInModal({
              title: 'Error',
              html: 'Unable to read color scheme.'
            });
          }).always(function () {
            $file.remove();
          });

        });
      } else {
        var savedColorSchemes = JSON.parse(localStorage.getItem('phantasus-colorScheme'));
        var scheme = JSON.parse(savedColorSchemes[val]);
        heatMap.heatmap.getColorScheme().fromJSON(scheme);
        // saved in local storage
      }
      if (repaint) {
        colorSchemeChooser
          .setColorScheme(heatMap.heatmap
            .getColorScheme());
        heatMap.heatmap.setInvalid(true);
        heatMap.heatmap.repaint();
      }
      colorSchemeChooser.restoreCurrentValue();
    });
  colorSchemeFormBuilder.$form
    .find('[name=color_by]')
    .on(
      'change',
      function (e) {
        var colorByField = $(this).val();
        if (colorByField == '(None)') {
          colorByField = null;
        }
        var colorByValue = null;
        heatMap.heatmap.getColorScheme()
          .setSeparateColorSchemeForRowMetadataField(
            colorByField);
        if (colorByField != null) {
          $colorByValue
            .html(phantasus.Util
              .createOptions(phantasus.VectorUtil
                .createValueToIndexMap(
                  heatMap.project
                    .getFullDataset()
                    .getRowMetadata()
                    .getByName(
                      colorByField))
                .keys()));
          colorByValue = $colorByValue.val();
        } else {
          $colorByValue.html('');
        }

        heatMap.heatmap.getColorScheme().setCurrentValue(
          colorByValue);
        colorSchemeChooser.setCurrentValue(colorByValue);
        heatMap.heatmap.setInvalid(true);
        heatMap.heatmap.repaint();
        colorSchemeChooser.setColorScheme(heatMap.heatmap
          .getColorScheme());
      });
  $colorByValue.on('change', function (e) {
    if (heatMap.heatmap.getColorScheme()
        .getSeparateColorSchemeForRowMetadataField() == null) {
      colorSchemeChooser.setCurrentValue(null);
      heatMap.heatmap.getColorScheme().setCurrentValue(null);
      colorSchemeChooser.setColorScheme(heatMap.heatmap
        .getColorScheme());
    } else {
      colorSchemeChooser.setCurrentValue($colorByValue.val());
      colorSchemeChooser.setColorScheme(heatMap.heatmap
        .getColorScheme());
    }
  });
  displayFormBuilder.$form.find('[name=column_size]').on(
    'keyup',
    _.debounce(function (e) {
      heatMap.heatmap.getColumnPositions().setSize(
        parseFloat($(this).val()));
      heatMap.revalidate();
      colorSchemeChooser.restoreCurrentValue();

    }, 100));
  displayFormBuilder.$form.find('[name=row_gap_size]').on('keyup',
    _.debounce(function (e) {
      heatMap.rowGapSize = parseFloat($(this).val());
      heatMap.revalidate();
      colorSchemeChooser.restoreCurrentValue();
    }, 100));
  displayFormBuilder.$form.find('[name=column_gap_size]').on('keyup',
    _.debounce(function (e) {
      heatMap.columnGapSize = parseFloat($(this).val());
      heatMap.revalidate();
      colorSchemeChooser.restoreCurrentValue();
    }, 100));
  displayFormBuilder.$form.find('[name=squish_factor]').on('keyup',
    _.debounce(function (e) {
      var f = parseFloat($(this).val());
      heatMap.heatmap.getColumnPositions().setSquishFactor(f);
      heatMap.heatmap.getRowPositions().setSquishFactor(f);
      heatMap.revalidate();
      colorSchemeChooser.restoreCurrentValue();
    }, 100));
  displayFormBuilder.$form.find('[name=row_dendrogram_line_thickness]').on(
    'keyup', _.debounce(function (e) {
      heatMap.rowDendrogram.lineWidth = parseFloat($(this).val());
      heatMap.revalidate();
      colorSchemeChooser.restoreCurrentValue();

    }, 100));
  displayFormBuilder.$form.find('[name=column_dendrogram_line_thickness]')
    .on(
      'keyup',
      _.debounce(function (e) {
        heatMap.columnDendrogram.lineWidth = parseFloat($(
          this).val());
        heatMap.revalidate();
        colorSchemeChooser.restoreCurrentValue();
      }, 100));
  var $tab = $('<div class="tab-content"></div>');
  $metadataDiv.appendTo($tab);
  $heatMapDiv.appendTo($tab);
  $displayDiv.appendTo($tab);
  var $div = $('<div></div>');
  var $ul = $('<ul class="nav nav-tabs" role="tablist">' + '<li><a href="#'
    + annotationOptionsTabId
    + '" role="tab" data-toggle="tab">Annotations</a></li>'
    + '<li><a href="#' + heatMapOptionsTabId
    + '" role="tab" data-toggle="tab">Color Scheme</a></li>'
    + '<li><a href="#' + displayOptionsTabId
    + '" role="tab" data-toggle="tab">Display</a></li>' + '</ul>');
  $ul.appendTo($div);
  $tab.appendTo($div);
  // set current scheme
  colorSchemeChooser.setColorScheme(heatMap.heatmap.getColorScheme());
  colorSchemeChooserUpdated();
  $ul.find('[role=tab]:eq(1)').tab('show');
  phantasus.FormBuilder.showInModal({
    title: 'Options',
    html: $div,
    close: 'Close',
    focus: heatMap.getFocusEl(),
    onClose: function () {
      $div.find('input').off('keyup');
      $ca.off('change');
      $ra.off('change');
      $div.remove();
      colorSchemeChooser.dispose();
    }
  });
};

phantasus.HeatMapSizer = function () {
  this.seriesName = null;
  this.sizeByScale = d3.scale.linear().domain([this.min, this.max])
    .range([0, 1]).clamp(true);
};
phantasus.HeatMapSizer.prototype = {
  min: 0,
  max: 1,
  copy: function () {
    var sizer = new phantasus.HeatMapSizer();
    sizer.seriesName = this.seriesName;
    sizer.min = this.min;
    sizer.max = this.max;
    sizer.sizeByScale = this.sizeByScale.copy();
    return sizer;
  },
  valueToFraction: function (value) {
    return this.sizeByScale(value);
  },
  setMin: function (min) {
    this.min = min;
    this.sizeByScale = d3.scale.linear().domain([this.min, this.max])
      .range([0, 1]).clamp(true);
  },
  setMax: function (max) {
    this.max = max;
    this.sizeByScale = d3.scale.linear().domain([this.min, this.max])
      .range([0, 1]).clamp(true);
  },
  getMin: function () {
    return this.min;
  },
  getMax: function () {
    return this.max;
  },
  getSeriesName: function () {
    return this.seriesName;
  },
  setSeriesName: function (name) {
    this.seriesName = name;
  }
};

phantasus.HeatMapToolBar = function (heatMap) {
  this.heatMap = heatMap;
  this.rowSearchResultModelIndices = [];
  this.columnSearchResultModelIndices = [];
  var _this = this;
  var layout = ['<div class="hidden-print">'];
  layout.push('<div data-name="toolbar"></div>');
  layout.push(
    '<div data-name="tip" style="white-space:nowrap; border-top: thin solid #e7e7e7;margin-bottom:2px;height: 14px; font-size: 10px;overflow:hidden;"></div>');
  layout.push('</div>');

  var $el = $(layout.join(''));
  var searchHtml = [];
  var $searchForm = $(
    '<form style="display:inline-block;margin-right:14px;" name="searchForm"' +
    ' class="form' +
    ' form-inline' +
    ' form-compact"' +
    ' role="search"></form>');
  $searchForm.on('submit', function (e) {
    e.preventDefault();
  });

  // toogle search buttons
  searchHtml.push('<div title="Toggle' +
    ' Search (' + phantasus.Util.COMMAND_KEY + '/)" class="btn-group"' +
    ' data-toggle="buttons">');
  searchHtml.push('<label class="btn btn-default btn-xxs">');
  searchHtml.push(
    '<input data-search="rows" type="radio" autocomplete="off" name="searchToggle"' +
    ' type="button"> Rows');
  searchHtml.push('</label>');

  searchHtml.push('<label class="btn btn-default btn-xxs">');
  searchHtml.push(
    '<input data-search="columns" type="radio" autocomplete="off" name="searchToggle"> Columns');
  searchHtml.push('</label>');

  searchHtml.push('<label class="btn btn-default btn-xxs">');
  searchHtml.push(
    '<input data-search="values" type="radio" autocomplete="off" name="searchToggle">' +
    ' Values');
  searchHtml.push('</label>');

  searchHtml.push('<label class="btn btn-default btn-xxs">');
  searchHtml.push(
    '<input data-search="rowDendrogram" type="radio" autocomplete="off"' +
    ' name="searchToggle"> Row Dendrogram');
  searchHtml.push('</label>');

  searchHtml.push('<label class="btn btn-default btn-xxs">');
  searchHtml.push(
    '<input data-search="columnDendrogram" type="radio" autocomplete="off"' +
    ' name="searchToggle"> Column Dendrogram');
  searchHtml.push('</label>');
  searchHtml.push('</div>');

  function createSearchOptionsMenu() {
    searchHtml.push('<div style="display:inline-block;" class="dropdown">');
    searchHtml.push(
      '<button type="button" class="btn btn-default btn-xxs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <span class="fa fa-caret-down"></span></button>');
    searchHtml.push('<ul data-name="searchOptions" class="dropdown-menu">');
    searchHtml.push(
      '<li><a data-group="matchMode" data-name="exact" href="#"><span' +
      ' data-type="toggle"></span>Exact' +
      ' Match</a></li>');
    searchHtml.push(
      '<li><a data-group="matchMode" data-name="contains" href="#"><span' +
      ' data-type="toggle"' +
      ' class="dropdown-checkbox fa fa-check"></span>Contains</a></li>');
    searchHtml.push('<li role="separator" class="divider"></li>');

    searchHtml.push(
      '<li><a data-group="searchMode" data-name="matchAny" href="#"><span' +
      ' data-type="toggle"' +
      ' class="dropdown-checkbox fa fa-check"></span>Match Any Search Term</a></li>');

    searchHtml.push(
      '<li><a data-group="searchMode" data-name="matchAll" href="#"><span' +
      ' data-type="toggle"></span>Match All Search Terms</a></li>');

    searchHtml.push('<li role="separator" class="divider"></li>');
    searchHtml.push('<li><a data-name="searchHelp" href="#">Help</a></li>');
    searchHtml.push('</ul>');
    searchHtml.push('</div>');
  }

  function createSearchMenu(dataName, navigation) {
    searchHtml.push(
      '<div style="display:inline-block;" data-name="' + dataName + '">');
    searchHtml.push('<div class="form-group">');
    searchHtml.push(
      '<input type="text" class="form-control input-sm" autocomplete="off"' +
      ' name="search">');
    searchHtml.push('</div>');
    searchHtml.push('<div class="form-group">');
    searchHtml.push(
      '<span data-name="searchResultsWrapper" style="display:none;">');
    searchHtml.push(
      '<span style="font-size:12px;" data-name="searchResults"></span>');
    if (navigation) {
      searchHtml.push(
        '<button name="previousMatch" type="button" class="btn btn-default btn-xxs" data-toggle="tooltip" title="Previous"><i class="fa fa-chevron-up"></i></button>');
      searchHtml.push(
        '<button name="nextMatch" type="button" class="btn btn-default btn-xxs" data-toggle="tooltip" title="Next"><i class="fa fa-chevron-down"></i></button>');
      searchHtml.push(
        '<button name="matchesToTop" type="button" class="btn btn-default btn-xxs" data-toggle="tooltip" title="Matches To Top"><i class="fa fa-level-up"></i></button>');
    }
    searchHtml.push('</span>');
    searchHtml.push('</div>');
    searchHtml.push('</div>');
    searchHtml.push('</div>');
  }

  if (heatMap.options.toolbar.searchRows ||
    heatMap.options.toolbar.searchColumns ||
    heatMap.options.toolbar.searchValues) {
    createSearchOptionsMenu();
  }
  if (heatMap.options.toolbar.searchRows) {
    createSearchMenu('searchRowsGroup', true);
  }
  if (heatMap.options.toolbar.searchColumns) {
    createSearchMenu('searchColumnsGroup', true);
  }

  if (heatMap.options.toolbar.searchValues) {
    createSearchMenu('searchValuesGroup', false);
  }
  createSearchMenu('searchRowDendrogramGroup', false);
  createSearchMenu('searchColumnDendrogramGroup', false);

  // dimensions
  if (heatMap.options.toolbar.dimensions) {
    searchHtml.push('<div class="form-group">');
    searchHtml.push(
      '<h6 style="display: inline; margin-left:10px;" data-name="dim"></h6>');
    searchHtml.push(
      '<h6 style="display: inline; margin-left:10px;" data-name="selection"></h6>');
    searchHtml.push('</div>');
  }

  var $menus = $(
    '<div style="display: inline-block;margin-right:14px;"></div>');

  function createMenu(menuName, actions, minWidth) {
    if (!minWidth) {
      minWidth = '0px';
    }
    var menu = [];
    var dropdownId = _.uniqueId('phantasus');
    menu.push('<div class="dropdown phantasus-menu">');
    menu.push(
      '<a class="dropdown-toggle phantasus-black-link phantasus-black-link-background" type="button"' +
      ' id="' + dropdownId +
      '" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">');
    menu.push(menuName);

    menu.push('</a>');
    menu.push('<ul style="min-width:' + minWidth +
      ';" class="dropdown-menu" aria-labelledby="' + dropdownId + '">');
    actions.forEach(function (name) {
      if (name == null) {
        menu.push('<li role="separator" class="divider"></li>');
      } else {
        var action = heatMap.getActionManager().getAction(name);
        if (action != null) {
          menu.push('<li>');
          menu.push(
            '<a class="phantasus-menu-item" data-action="' + action.name +
            '" href="#">');
          menu.push(action.name);
          if (action.ellipsis) {
            menu.push('...');
          }
          if (action.icon) {
            menu.push('<span class="' + action.icon +
              ' phantasus-menu-item-icon"></span> ');
          }
          if (action.which) {
            menu.push('<span class="pull-right">');
            if (action.commandKey) {
              menu.push(phantasus.Util.COMMAND_KEY);
            }
            if (action.shiftKey) {
              menu.push('Shift+');
            }
            menu.push(phantasus.KeyboardCharMap[action.which[0]]);
            menu.push('</span>');
          }

          menu.push('</a>');
          menu.push('</li>');
        }
      }
    });

    menu.push('</ul>');
    menu.push('</div>');
    $(menu.join('')).appendTo($menus);
  }

  //// console.log("HeatMapToolbar ::", "heatMap:", heatMap, "heatMap.options:", heatMap.options);
  if (heatMap.options.menu) {
    if (heatMap.options.menu.File) {
      createMenu('File', heatMap.options.menu.File, '240px');
    }
    if (heatMap.options.menu.View) {
      createMenu('Edit', heatMap.options.menu.Edit);
    }
    if (heatMap.options.menu.View) {
      createMenu('View', heatMap.options.menu.View, '170px');
    }
    if (heatMap.options.menu.Tools) {
      createMenu('Tools', heatMap.options.menu.Tools);
    }
    if (heatMap.options.menu.Help) {
      createMenu('Help', heatMap.options.menu.Help, '220px');
    }
  }

  $(searchHtml.join('')).appendTo($searchForm);
  var $lineOneColumn = $el.find('[data-name=toolbar]');

  $menus.appendTo($lineOneColumn);
  $searchForm.appendTo($lineOneColumn);
  var toolbarHtml = ['<div style="display: inline;">'];
  toolbarHtml.push('<div class="phantasus-button-divider"></div>');
  // zoom
  if (heatMap.options.toolbar.zoom) {

    var dropdownId = _.uniqueId('phantasus');
    toolbarHtml.push('<div style="display:inline-block;" class="dropdown">');
    toolbarHtml.push(
      '<a class="dropdown-toggle phantasus-black-link" type="button" id="' +
      dropdownId +
      '" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">');
    // toolbarHtml.push('<input style="width:2em;height:21px;" id="' + dropdownId + '" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">');
    toolbarHtml.push('<button type="button"' +
      ' class="btn btn-default btn-xxs"><span class="fa' +
      ' fa-search-plus"></span>');
    toolbarHtml.push(
      ' <span style="font-size: .8em;" class="fa fa-caret-down"></span>');
    toolbarHtml.push('</button>');
    toolbarHtml.push(
      '<ul style="width:200px;" class="dropdown-menu" aria-labelledby="' + dropdownId + '">');
    toolbarHtml.push(
      '<li><a class="phantasus-menu-item" href="#" data-action="Zoom In">Zoom In<span class="pull-right">+</span></a></li>');
    toolbarHtml.push(
      '<li><a class="phantasus-menu-item" href="#" data-action="Zoom Out">Zoom' +
      ' Out<span class="pull-right">-</span></a></li>');
    toolbarHtml.push('<li role="separator" class="divider"></li>');
    toolbarHtml.push(
      '<li><a class="phantasus-menu-item" href="#" data-action="Fit To Window">Fit To Window<span' +
      ' class="fa' +
      ' fa-compress phantasus-menu-item-icon"></span><span class="pull-right">' +
      phantasus.Util.COMMAND_KEY +
      phantasus.KeyboardCharMap[heatMap.getActionManager().getAction('Fit To Window').which[0]] + '</span> </a></li>');
    toolbarHtml.push(
      '<li><a class="phantasus-menu-item" href="#" data-action="Fit Rows To Window">Fit Rows To Window</a></li>');
    toolbarHtml.push(
      '<li><a class="phantasus-menu-item" href="#" data-action="Fit Columns To Window">Fit Columns To Window</a></li>');
    toolbarHtml.push('<li role="separator" class="divider"></li>');
    toolbarHtml.push(
      '<li><a class="phantasus-menu-item" href="#" data-action="100%">100%</a></li>');
    toolbarHtml.push('</ul>');
    toolbarHtml.push('</div>');
  }
  toolbarHtml.push('<div class="phantasus-button-divider"></div>');
  if (heatMap.options.toolbar.sort) {
    toolbarHtml.push(
      '<button data-toggle="tooltip" title="Sort" name="Sort" type="button" class="btn' +
      ' btn-default btn-xxs"><span class="fa fa-sort-alpha-asc"></span></button>');
  }
  if (heatMap.options.toolbar.options) {
    toolbarHtml.push(
      '<button data-action="Options" data-toggle="tooltip" title="Options" type="button"' +
      ' class="btn btn-default btn-xxs"><span class="fa fa-cog"></span></button>');

  }

  toolbarHtml.push('<div class="phantasus-button-divider"></div>');
  if (heatMap.options.toolbar.openFile) {
    toolbarHtml.push(
      '<button data-action="Open File" data-toggle="tooltip" title="Open File ('
      + phantasus.Util.COMMAND_KEY
      +
      'O)" type="button" class="btn btn-default btn-xxs"><span class="fa fa-folder-open-o"></span></button>');
  }
  if (heatMap.options.toolbar.saveImage) {
    toolbarHtml.push(
      '<button data-action="Save Image" data-toggle="tooltip" title="Save Image ('
      + phantasus.Util.COMMAND_KEY
      +
      'S)" type="button" class="btn btn-default btn-xxs"><span class="fa fa-file-image-o"></span></button>');
  }
  if (heatMap.options.toolbar.saveDataset) {
    toolbarHtml.push(
      '<button data-action="Save Dataset" data-toggle="tooltip" title="Save Dataset ('
      + phantasus.Util.COMMAND_KEY
      +
      'Shift+S)" type="button" class="btn btn-default btn-xxs"><span class="fa fa-floppy-o"></span></button>');
  }
  if (heatMap.options.toolbar.saveSession) {
    toolbarHtml.push(
      '<button data-action="Save Session" data-toggle="tooltip" title="Save Session" type="button"' +
      ' class="btn btn-default btn-xxs"><span class="fa fa-anchor"></span></button>');
  }

  toolbarHtml.push('<div class="phantasus-button-divider"></div>');
  if (heatMap.options.toolbar.filter) {
    toolbarHtml.push(
      '<button data-action="Filter" data-toggle="tooltip" title="Filter" type="button"' +
      ' class="btn btn-default btn-xxs"><span class="fa fa-filter"></span></button>');
  }
  if (heatMap.options.toolbar.chart && typeof echarts !== 'undefined') {
    toolbarHtml.push(
      '<button data-action="Chart" data-toggle="tooltip" title="Chart" type="button" class="btn' +
      ' btn-default btn-xxs"><span class="fa fa-line-chart"></span></button>');

  }
  // legend
  if (heatMap.options.toolbar.colorKey) {
    toolbarHtml.push('<div class="phantasus-button-divider"></div>');
    toolbarHtml.push('<div class="btn-group">');
    toolbarHtml.push(
      '<button type="button" class="btn btn-default btn-xxs" data-toggle="dropdown"><span title="Color Key" data-toggle="tooltip" class="fa fa-key"></span></button>');
    toolbarHtml.push('<ul data-name="key" class="dropdown-menu" role="menu">');
    toolbarHtml.push('<li data-name="keyContent"></li>');
    toolbarHtml.push('</ul>');
    toolbarHtml.push('</div>');
    toolbarHtml.push('<div class="phantasus-button-divider"></div>');
  }
  toolbarHtml.push('</div>');
  var $toolbar = $(toolbarHtml.join(''));

  $toolbar.find('[data-action]').on('click', function (e) {
    e.preventDefault();
    heatMap.getActionManager().execute($(this).data('action'));
  }).on('blur', function (e) {
    if (document.activeElement === document.body) {
      heatMap.focus();
    }
  });
  $menus.on('click', 'li > a', function (e) {
    e.preventDefault();
    heatMap.getActionManager().execute($(this).data('action'));
  }).on('blur', function (e) {
    if (document.activeElement === document.body) {
      heatMap.focus();
    }
  });
  if (heatMap.options.toolbar.$customButtons) {
    heatMap.options.toolbar.$customButtons.appendTo($toolbar);
  }
  $toolbar.appendTo($lineOneColumn);
  // $hide.appendTo($el.find('[data-name=toggleEl]'));
  $el.prependTo(heatMap.$content);
  this.$tip = $el.find('[data-name=tip]');

  $el.find('[data-toggle="tooltip"]').tooltip({
    placement: 'bottom',
    container: 'body',
    trigger: 'hover'
  }).on('click', function () {
    $(this).tooltip('hide');
  });
  var $key = $el.find('[data-name=key]');
  var $keyContent = $el.find('[data-name=keyContent]');
  $key.dropdown().parent().on('show.bs.dropdown', function () {
    new phantasus.HeatMapColorSchemeLegend(heatMap, $keyContent);
    phantasus.Util.trackEvent({
      eventCategory: 'ToolBar',
      eventAction: 'colorKey'
    });
  });

  var searchHelpHtml = [];
  searchHelpHtml.push('<h4>Symbols</h4>');
  searchHelpHtml.push('<table class="table table-bordered">');
  searchHelpHtml.push('<tr><th>Term</th><th>Description</th></tr>');
  searchHelpHtml.push(
    '<tr><td><code><strong>*</strong></code></td><td>Quote a search term for an' +
    ' exact' +
    ' match. <br' +
    ' />Example: <code><strong>"root beer"</strong></code></td></tr>');

  searchHelpHtml.push(
    '<tr><td><code><strong>-</strong></code></td><td>Exclude matches using -' +
    ' modifier.</td></tr>');
  searchHelpHtml.push(
    '<tr><td><code><strong>..</strong></code></td><td>Separate numbers by two' +
    ' periods' +
    ' without spaces to' +
    ' see numbers that fall within a range.. <br' +
    ' />Example: <code><strong>1..10</strong></code></td></tr>');
  searchHelpHtml.push(
    '<tr><td><code><strong><= < > >= =</strong></code></td><td>Perform a' +
    ' numeric' +
    ' search.' +
    ' <br' +
    ' />Example: <code><strong>>4</strong></code></td></tr>');
  searchHelpHtml.push('</table>');
  searchHelpHtml.push('<h4>Search fields</h4>');
  searchHelpHtml.push(
    '<p>You can restrict your search to any field by typing the field name followed by a colon ":" and then the term you are looking for. For example, to search for matches containing "beer" in the beverage field, you can enter:' +
    ' <code><strong>beverage:beer</strong></code>');
  searchHelpHtml.push(
    'Note that searches only include metadata fields that are displayed. You' +
    ' can search a hidden field by performing a field search.');

  // searchHelpHtml.push('<br />Note: The field is only valid for the term that it directly' +
  // 	' precedes.');
  searchHelpHtml.push(
    '<p>You can search for an exact list of values by enclosing the list of' +
    ' values in parentheses. For example: <code><strong>pet:(cat dog)</strong></code>' +
    ' searches all pets that are either cats or dogs.</p>');
  var $searchHelp = $(searchHelpHtml.join(''));
  $el.find('[data-name=searchHelp]').on('click', function (e) {
    e.preventDefault();
    phantasus.FormBuilder.showInModal({
      title: 'Search Help',
      html: $searchHelp,
      appendTo: heatMap.getContentEl(),
      focus: heatMap.getFocusEl()
    });
  });
  var $searchRowsGroup = $searchForm.find('[data-name=searchRowsGroup]');
  var $searchColumnsGroup = $searchForm.find('[data-name=searchColumnsGroup]');
  var $searchValuesGroup = $searchForm.find('[data-name=searchValuesGroup]');
  var $searchRowDendrogramGroup = $searchForm.find(
    '[data-name=searchRowDendrogramGroup]');
  var $searchColumnDendrogramGroup = $searchForm.find(
    '[data-name=searchColumnDendrogramGroup]');

  this.$searchRowDendrogramGroup = $searchRowDendrogramGroup;
  this.$searchColumnDendrogramGroup = $searchColumnDendrogramGroup;
  this.matchMode = 'contains';
  this.matchAllPredicates = false;
  var $searchToggle = $searchForm.find('[name=searchToggle]'); // buttons
  var nameToSearchObject = {};

  function getSearchElements($group, searchName, cb) {
    var obj = {
      $group: $group,
      $search: $group.find('[name=search]'),
      $searchResultsWrapper: $group.find('[data-name=searchResultsWrapper]'),
      $searchResults: $group.find('[data-name=searchResults]'),
      $previousMatch: $group.find('[name=previousMatch]'),
      $nextMatch: $group.find('[name=nextMatch]'),
      $matchesToTop: $group.find('[name=matchesToTop]'),
      $toggleButton: $searchToggle.filter('[data-search=' + searchName + ']').parent()
    };
  nameToSearchObject[searchName] = obj;
    return obj;
  }  var $searchOptions = $el.find('[data-name=searchOptions]');
    $searchOptions.on('click', 'li > a', function (e) {
      e.preventDefault();
      var $this = $(this);
      var group = $this.data('group');
      if (group === 'matchMode') {
        _this.matchMode = $this.data('name');
      } else {
        _this.matchAllPredicates = $this.data('name') === 'matchAll';
      }
      var $searchField;
    if (_this.rowSearchObject.$search.is(':visible')) {
      $searchField = _this.rowSearchObject.$search;
    } else if (_this.columnSearchObject.$search.is(':visible')) {
      $searchField = _this.rowSearchObject.$search;
    } else if (_this.rowDendrogramSearchObject.$search.is(':visible')) {
      $searchField = _this.rowSearchObject.$search;
    } else if (_this.columnDendrogramSearchObject.$search.is(':visible')) {
      $searchField = _this.rowSearchObject.$search;
    } else if (_this.valueSearchObject.$search.is(':visible')) {
      $searchField = _this.rowSearchObject.$search;
    }
    if ($searchField) {
      $searchField.trigger($.Event('keyup', {
        keyCode: 13,
        which: 13
      }));
      // trigger search again
    }

    var $span = $(this).find('span');
    if ($span.data('type') === 'toggle') {
      $searchOptions.find('[data-group=' + group + '] > [data-type=toggle]').removeClass('dropdown-checkbox' +
        ' fa' +
        ' fa-check');
      $span.addClass('dropdown-checkbox fa fa-check');
    }
    phantasus.Util.trackEvent({
      eventCategory: 'ToolBar',
      eventAction: 'searchMatchMode'
    });
  });
  this.rowSearchObject = getSearchElements($searchRowsGroup, 'rows',
    function () {
      _this.search(true);
    });
  this.columnSearchObject = getSearchElements($searchColumnsGroup, 'columns',
    function () {
      _this.search(false);
    });
  this.rowDendrogramSearchObject = getSearchElements($searchRowDendrogramGroup,
    'rowDendrogram', function () {
      _this.searchDendrogram(false);
    });
  this.columnDendrogramSearchObject = getSearchElements(
    $searchColumnDendrogramGroup, 'columnDendrogram', function () {
      _this.searchDendrogram(false);
    });
  this.valueSearchObject = getSearchElements($searchValuesGroup, 'values',
    function () {
      searchValues();
    });

  // set button and search controls visibility
  if (!heatMap.options.toolbar.searchRows) {
    this.rowSearchObject.$toggleButton.hide();
    this.rowSearchObject.$group.css('display', 'none');
  }

  if (!heatMap.options.toolbar.searchColumns) {
    this.columnSearchObject.$toggleButton.hide();
    this.columnSearchObject.$group.css('display', 'none');
  }
  if (!heatMap.options.toolbar.searchValues) {
    this.valueSearchObject.$toggleButton.hide();
    this.valueSearchObject.$group.css('display', 'none');
  }
  this.rowDendrogramSearchObject.$toggleButton.hide();
  this.rowDendrogramSearchObject.$group.hide();

  this.columnDendrogramSearchObject.$toggleButton.hide();
  this.columnDendrogramSearchObject.$group.hide();

  this.rowDendrogramSearchObject.$searchResultsWrapper.show();
  this.columnDendrogramSearchObject.$searchResultsWrapper.show();
  this.valueSearchObject.$searchResultsWrapper.show();

  this.rowSearchObject.$search.css({
    'border-top': '3.8px solid #e6e6e6',
    'border-bottom': '3.8px solid #e6e6e6',
    width: '240px'
  });

  this.columnSearchObject.$search.css({
    'border-right': '3.8px solid #e6e6e6',
    'border-left': '3.8px solid #e6e6e6',
    width: '240px'
  });

  this.$valueSearchResults = $searchValuesGroup.find('[name=searchResults]');
  this.$valueTextField = $searchValuesGroup.find('[name=search]');
  this.$dimensionsLabel = $el.find('[data-name=dim]');
  this.$selectionLabel = $el.find('[data-name=selection]');

  $searchToggle.on('change', function (e) {
    var search = $(this).data('search');
    for (var name in nameToSearchObject) {
      var searchObject = nameToSearchObject[name];
      if (name === search) {
        searchObject.$group.css('display', 'inline-block');
        searchObject.$search.focus();
      } else {
        searchObject.$group.css('display', 'none');
      }
    }
  });

  this.toggleSearch = function () {
    var $visible = $searchToggle.filter(':visible');
    var $checked = $searchToggle.filter(':checked');
    var $next = $visible.eq($visible.index($checked) + 1);
    if (!$next.length) {
      $next = $visible.first();
    }
    $next.click();
  };

  for (var i = 0; i < $searchToggle.length; i++) {
    var $button = $($searchToggle[i]);
    if ($button.parent().css('display') === 'block') {
      $button.click();
      break;
    }
  }

  heatMap.on('dendrogramAnnotated', function (e) {
    if (e.isColumns) { // show buttons
      _this.rowDendrogramSearchObject.$toggleButton.show();
    } else {
      _this.columnDendrogramSearchObject.$toggleButton.show();
    }
  });
  heatMap.on('dendrogramChanged', function (e) {
    if (e.isColumns) {
      _this.rowDendrogramSearchObject.$group.hide();
      _this.rowDendrogramSearchObject.$toggleButton.hide();
    } else {
      _this.columnDendrogramSearchObject.$group.hide();
      _this.columnDendrogramSearchObject.$toggleButton.hide();
    }
  });
  var project = heatMap.getProject();

  phantasus.Util.autosuggest({
    $el: this.rowSearchObject.$search,
    filter: function (terms, cb) {
      var indices = [];
      var meta = project.getSortedFilteredDataset().getRowMetadata();
      heatMap.getVisibleTrackNames(false).forEach(function (name) {
        var index = phantasus.MetadataUtil.indexOf(meta, name);
        if (index !== -1) {
          indices.push(index);
        }
      });
      meta = new phantasus.MetadataModelColumnView(meta, indices);
      phantasus.MetadataUtil.autocomplete(meta)(terms, cb);
    },
    select: function () {
      _this.search(true);
    }
  });

  this.rowSearchObject.$search.on('keyup', _.debounce(function (e) {
    if (e.which === 13) {
      e.preventDefault();
    }
    _this.search(true);
    phantasus.Util.trackEvent({
      eventCategory: 'ToolBar',
      eventAction: 'searchRows'
    });
  }, 500));
  phantasus.Util.autosuggest({
    $el: this.columnSearchObject.$search,
    filter: function (terms, cb) {
      var indices = [];
      var meta = project.getSortedFilteredDataset().getColumnMetadata();
      heatMap.getVisibleTrackNames(true).forEach(function (name) {
        var index = phantasus.MetadataUtil.indexOf(meta, name);
        if (index !== -1) {
          indices.push(index);
        }
      });
      meta = new phantasus.MetadataModelColumnView(meta, indices);
      phantasus.MetadataUtil.autocomplete(meta)(terms, cb);
    },
    select: function () {
      _this.search(false);
    }
  });
  this.columnSearchObject.$search.on('keyup', _.debounce(function (e) {
    if (e.which === 13) {
      e.preventDefault();
    }
    _this.search(false);
    phantasus.Util.trackEvent({
      eventCategory: 'ToolBar',
      eventAction: 'searchColumns'
    });
  }, 500));

  // dendrogram search

  phantasus.Util.autosuggest({
    $el: this.rowDendrogramSearchObject.$search,
    filter: function (tokens, cb) {
      var d = heatMap.getDendrogram(false);
      if (!d.searchTerms) {
        cb([]);
      } else {
        var token = tokens != null && tokens.length > 0
          ? tokens[tokens.selectionStartIndex]
          : '';
        token = $.trim(token);
        if (token === '') {
          cb([]);
        } else {
          phantasus.Util.autocompleteArrayMatcher(token, cb, d.searchTerms, null,
            10);
        }
      }
    },
    select: function () {
      _this.searchDendrogram(false);
    }
  });

  this.rowDendrogramSearchObject.$search.on('keyup', _.debounce(function (e) {
    if (e.which === 13) {
      e.preventDefault();
    }
    _this.searchDendrogram(false);
    phantasus.Util.trackEvent({
      eventCategory: 'ToolBar',
      eventAction: 'searchRowDendrogram'
    });
  }, 500));

  phantasus.Util.autosuggest({
    $el: this.columnDendrogramSearchObject.$search,
    filter: function (tokens, cb) {
      var d = heatMap.getDendrogram(true);
      if (!d.searchTerms) {
        cb([]);
      } else {
        var token = tokens != null && tokens.length > 0
          ? tokens[tokens.selectionStartIndex]
          : '';
        token = $.trim(token);
        if (token === '') {
          cb([]);
        } else {
          phantasus.Util.autocompleteArrayMatcher(token, cb, d.searchTerms, null,
            10);
        }
      }
    },
    select: function () {
      _this.searchDendrogram(true);
    }
  });

  this.columnDendrogramSearchObject.$search.on('keyup',
    _.debounce(function (e) {
      if (e.which === 13) {
        e.preventDefault();
      }
      _this.searchDendrogram(true);
      phantasus.Util.trackEvent({
        eventCategory: 'ToolBar',
        eventAction: 'searchColumnDendrogram'
      });
    }, 500));

  function searchValues() {
    var $searchResultsLabel = _this.$valueSearchResults;
    var text = $.trim(_this.$valueTextField.val());
    if (text === '') {
      $searchResultsLabel.html('');
      project.getElementSelectionModel().setViewIndices(null);
    } else {
      var viewIndices = phantasus.DatasetUtil.searchValues({
        dataset: project.getSortedFilteredDataset(),
        text: text,
        matchAllPredicates: _this.matchAllPredicates,
        defaultMatchMode: _this.matchMode
      });

      project.getElementSelectionModel().setViewIndices(viewIndices);
      $searchResultsLabel.html(viewIndices.size() + ' match'
        + (viewIndices.size() === 1 ? '' : 'es'));
    }
  }

  phantasus.Util.autosuggest({
    $el: this.$valueTextField,
    filter: function (terms, cb) {
      phantasus.DatasetUtil.autocompleteValues(
        project.getSortedFilteredDataset())(terms, cb);
    },
    select: function () {
      searchValues();
    }
  });

  this.$valueTextField.on('keyup', _.debounce(function (e) {
    if (e.which === 13) {
      _this.$valueTextField.autocomplete('close');
      e.preventDefault();
    }
    searchValues();
  }, 500));

  this.toggleControls = function () {
    if ($lineOneColumn.css('display') === 'none') {
      $lineOneColumn.css('display', '');
      _this.rowSearchObject.$search.focus();
    } else {
      $lineOneColumn.css('display', 'none');
      $(_this.heatMap.heatmap.canvas).focus();
    }
  };
  this.$el = $el;
  var updateFilterStatus = function () {
    if (heatMap.getProject().getRowFilter().isEnabled()
      || heatMap.getProject().getColumnFilter().isEnabled()) {
      _this.$el.find('[name=filterButton]').addClass('btn-primary');
    } else {
      _this.$el.find('[name=filterButton]').removeClass('btn-primary');
    }

  };
  updateFilterStatus();

  this.columnSearchObject.$matchesToTop.on(
    'click',
    function (e) {
      e.preventDefault();
      var $this = $(this);
      $this.toggleClass('btn-primary');
      _this.setSelectionOnTop({
        isColumns: true,
        isOnTop: $this.hasClass('btn-primary'),
        updateButtonStatus: false
      });
      phantasus.Util.trackEvent({
        eventCategory: 'ToolBar',
        eventAction: 'columnMatchesToTop'
      });
    });
  this.rowSearchObject.$matchesToTop.on(
    'click',
    function (e) {
      e.preventDefault();
      var $this = $(this);
      $this.toggleClass('btn-primary');
      _this.setSelectionOnTop({
        isColumns: false,
        isOnTop: $this.hasClass('btn-primary'),
        updateButtonStatus: false
      });
      phantasus.Util.trackEvent({
        eventCategory: 'ToolBar',
        eventAction: 'rowMatchesToTop'
      });
    });
  project.on('rowSortOrderChanged.phantasus', function (e) {
    if (_this.searching) {
      return;
    }
    _this._updateSearchIndices(false);
    _this.rowSearchObject.$matchesToTop.removeClass('btn-primary');
  });

  project.on('columnSortOrderChanged.phantasus', function (e) {
    if (_this.searching) {
      return;
    }
    _this._updateSearchIndices(true);
    _this.columnSearchObject.$matchesToTop.removeClass('btn-primary');
  });

  heatMap.getProject().on('rowFilterChanged.phantasus', function (e) {
    _this.search(true);
    updateFilterStatus();
  });
  heatMap.getProject().on('columnFilterChanged.phantasus', function (e) {
    _this.search(false);
    updateFilterStatus();
  });
  heatMap.getProject().on('datasetChanged.phantasus', function () {
    _this.search(true);
    _this.search(false);
    updateFilterStatus();
  });
  heatMap.getProject().getRowSelectionModel().on(
    'selectionChanged.phantasus', function () {
      _this.updateSelectionLabel();
    });
  heatMap.getProject().getColumnSelectionModel().on(
    'selectionChanged.phantasus', function () {
      _this.updateSelectionLabel();
    });
  this.rowSearchResultViewIndicesSorted = null;
  this.currentRowSearchIndex = 0;
  this.columnSearchResultViewIndicesSorted = null;
  this.currentColumnSearchIndex = -1;
  this.columnSearchObject.$previousMatch.on(
    'click',
    function () {
      _this.currentColumnSearchIndex--;
      if (_this.currentColumnSearchIndex < 0) {
        _this.currentColumnSearchIndex = _this.columnSearchResultViewIndicesSorted.length -
          1;
      }
      heatMap.scrollLeft(
        heatMap.getHeatMapElementComponent().getColumnPositions().getPosition(
          _this.columnSearchResultViewIndicesSorted[_this.currentColumnSearchIndex]));
      phantasus.Util.trackEvent({
        eventCategory: 'ToolBar',
        eventAction: 'previousColumnMatch'
      });
    });
  this.rowSearchObject.$previousMatch.on(
    'click',
    function () {
      _this.currentRowSearchIndex--;
      if (_this.currentRowSearchIndex < 0) {
        _this.currentRowSearchIndex = _this.rowSearchResultViewIndicesSorted.length -
          1;
      }
      heatMap.scrollTop(
        heatMap.getHeatMapElementComponent().getRowPositions().getPosition(
          _this.rowSearchResultViewIndicesSorted[_this.currentRowSearchIndex]));
      phantasus.Util.trackEvent({
        eventCategory: 'ToolBar',
        eventAction: 'previousRowMatch'
      });
    });
  this.columnSearchObject.$nextMatch.on('click', function () {
    _this.next(true);
    phantasus.Util.trackEvent({
      eventCategory: 'ToolBar',
      eventAction: 'nextColumnMatch'
    });

  });
  this.rowSearchObject.$nextMatch.on('click', function () {
    _this.next(false);
    phantasus.Util.trackEvent({
      eventCategory: 'ToolBar',
      eventAction: 'nextRowMatch'
    });
  });
  this.updateDimensionsLabel();
  this.updateSelectionLabel();
}
;
phantasus.HeatMapToolBar.HIGHLIGHT_SEARCH_MODE = 0;
phantasus.HeatMapToolBar.FILTER_SEARCH_MODE = 1;
phantasus.HeatMapToolBar.MATCHES_TO_TOP_SEARCH_MODE = 2;
phantasus.HeatMapToolBar.SELECT_MATCHES_SEARCH_MODE = 3;
phantasus.HeatMapToolBar.prototype = {
  quickColumnFilter: false,
  searching: false,
  rowSearchMode: phantasus.HeatMapToolBar.SELECT_MATCHES_SEARCH_MODE,
  columnSearchMode: phantasus.HeatMapToolBar.SELECT_MATCHES_SEARCH_MODE,
  _updateSearchIndices: function (isColumns) {
    var project = this.heatMap.getProject();
    if (isColumns) {
      var viewIndices = [];
      var modelIndices = this.columnSearchResultModelIndices;
      for (var i = 0, length = modelIndices.length; i < length; i++) {
        var index = project.convertModelColumnIndexToView(modelIndices[i]);
        if (index !== -1) {
          viewIndices.push(index);
        }
      }
      viewIndices.sort(function (a, b) {
        return a < b ? -1 : 1;
      });
      this.columnSearchResultViewIndicesSorted = viewIndices;
      this.currentColumnSearchIndex = -1;
    } else {
      var viewIndices = [];
      var modelIndices = this.rowSearchResultModelIndices;
      for (var i = 0, length = modelIndices.length; i < length; i++) {
        var index = project.convertModelRowIndexToView(modelIndices[i]);
        if (index !== -1) {
          viewIndices.push(index);
        }
      }
      viewIndices.sort(function (a, b) {
        return a < b ? -1 : 1;
      });
      this.rowSearchResultViewIndicesSorted = viewIndices;
      this.currentRowSearchIndex = -1;
    }
  },
  next: function (isColumns) {
    var heatMap = this.heatMap;
    if (isColumns) {
      this.currentColumnSearchIndex++;
      if (this.currentColumnSearchIndex >=
        this.columnSearchResultViewIndicesSorted.length) {
        this.currentColumnSearchIndex = 0;
      }
      heatMap.scrollLeft(
        heatMap.getHeatMapElementComponent().getColumnPositions().getPosition(
          this.columnSearchResultViewIndicesSorted[this.currentColumnSearchIndex]));
    } else {
      this.currentRowSearchIndex++;
      if (this.currentRowSearchIndex >=
        this.rowSearchResultViewIndicesSorted.length) {
        this.currentRowSearchIndex = 0;
      }
      heatMap.scrollTop(
        heatMap.getHeatMapElementComponent().getRowPositions().getPosition(
          this.rowSearchResultViewIndicesSorted[this.currentRowSearchIndex]));
    }
  },
  getSearchField: function (type) {
    if (type === phantasus.HeatMapToolBar.COLUMN_SEARCH_FIELD) {
      return this.columnSearchObject.$search;
    } else if (type === phantasus.HeatMapToolBar.ROW_SEARCH_FIELD) {
      return this.rowSearchObject.$search;
    } else if (type ===
      phantasus.HeatMapToolBar.COLUMN_DENDROGRAM_SEARCH_FIELD) {
      return this.columnDendrogramSearchObject.$search;
    } else if (type === phantasus.HeatMapToolBar.ROW_DENDROGRAM_SEARCH_FIELD) {
      return this.rowDendrogramSearchObject.$search;
    }
  },
  setSearchText: function (options) {
    var $tf = options.isColumns ? this.columnSearchObject.$search
      : this.rowSearchObject.$search;
    var existing = options.append ? $.trim($tf.val()) : '';
    if (existing !== '') {
      existing += ' ';
    }
    if (options.onTop) {
      options.isColumns ? this.columnSearchObject.$matchesToTop.addClass(
        'btn-primary') : this.rowSearchObject.$matchesToTop.addClass(
        'btn-primary');

    }
    $tf.val(existing + options.text);
    this.search(!options.isColumns);
    if (options.scrollTo) {
      this.next(options.isColumns);
      // click next
    }
  },
  updateDimensionsLabel: function () {
    var p = this.heatMap.getProject();
    var d = p.getFullDataset();
    var f = p.getSortedFilteredDataset();
    var text = [];

    if (f.getRowCount() !== d.getRowCount()) {
      text.push('<b>');
      text.push(phantasus.Util.intFormat(f.getRowCount()));
      text.push('/');
      text.push(phantasus.Util.intFormat(d.getRowCount()));
      text.push('</b>');
    } else {
      text.push(phantasus.Util.intFormat(f.getRowCount()));
    }

    text.push(' rows by ');
    if (f.getColumnCount() !== d.getColumnCount()) {
      text.push('<b>');
      text.push(phantasus.Util.intFormat(f.getColumnCount()));
      text.push('/');
      text.push(phantasus.Util.intFormat(d.getColumnCount()));
      text.push('</b>');
    } else {
      text.push(phantasus.Util.intFormat(f.getColumnCount()));
    }

    text.push(' columns');
    this.$dimensionsLabel.html(text.join(''));
  },
  updateSelectionLabel: function () {
    var nc = this.heatMap.getProject().getColumnSelectionModel().count();
    var nr = this.heatMap.getProject().getRowSelectionModel().count();
    var text = [];
    text.push(phantasus.Util.intFormat(nr) + ' row');
    if (nr !== 1) {
      text.push('s');
    }
    text.push(', ');
    text.push(phantasus.Util.intFormat(nc) + ' column');
    if (nc !== 1) {
      text.push('s');
    }
    text.push(' selected');
    this.$selectionLabel.html(text.join(''));
  },
  searchDendrogram: function (isColumns) {
    var searchObject = isColumns
      ? this.columnDendrogramSearchObject
      : this.rowDendrogramSearchObject;
    var text = $.trim(searchObject.$search.val());
    var dendrogram = isColumns ? this.heatMap.columnDendrogram
      : this.heatMap.rowDendrogram;
    var $searchResults = searchObject.$searchResults;
    var matches = phantasus.DendrogramUtil.search({
      rootNode: dendrogram.tree.rootNode,
      text: text,
      matchAllPredicates: this.matchAllPredicates,
      defaultMatchMode: this.matchMode
    });
    if (matches === -1) {
      $searchResults.html('');
    } else {
      $searchResults.html(matches + ' match'
        + (matches === 1 ? '' : 'es'));
    }
    if (matches <= 0) {
      var positions = isColumns ? this.heatMap.getHeatMapElementComponent().getColumnPositions()
        : this.heatMap.getHeatMapElementComponent().getRowPositions();
      positions.setSquishedIndices(null);
      if (isColumns) {
        this.heatMap.getProject().setGroupColumns([], true);
      } else {
        this.heatMap.getProject().setGroupRows([], true);
      }
      positions.setSize(isColumns ? this.heatMap.getFitColumnSize()
        : this.heatMap.getFitRowSize());
    } else {
      phantasus.DendrogramUtil.squishNonSearchedNodes(this.heatMap,
        isColumns);
    }
    this.heatMap.updateDataset(); // need to update spaces for group
    // by
    this.heatMap.revalidate();
  },
  search: function (isRows) {
    this.searching = true;
    var isMatchesOnTop = isRows ? this.rowSearchObject.$matchesToTop.hasClass(
      'btn-primary') : this.columnSearchObject.$matchesToTop.hasClass(
      'btn-primary');
    var heatMap = this.heatMap;
    var project = heatMap.getProject();

    var sortKeys = isRows
      ? project.getRowSortKeys()
      : project.getColumnSortKeys();
    sortKeys = sortKeys.filter(function (key) {
      return !(key instanceof phantasus.MatchesOnTopSortKey &&
      key.toString() === 'matches on top');
    });

    var dataset = project.getSortedFilteredDataset();
    var $searchResultsLabel = isRows
      ? this.rowSearchObject.$searchResults
      : this.columnSearchObject.$searchResults;
    var searchText = !isRows
      ? $.trim(this.columnSearchObject.$search.val())
      : $.trim(this.rowSearchObject.$search.val());

    var metadata = isRows
      ? dataset.getRowMetadata()
      : dataset.getColumnMetadata();
    var visibleIndices = [];
    heatMap.getVisibleTrackNames(!isRows).forEach(function (name) {
      var index = phantasus.MetadataUtil.indexOf(metadata, name);
      if (index !== -1) {
        visibleIndices.push(index);
      }
    });
    var fullModel = metadata;
    metadata = new phantasus.MetadataModelColumnView(metadata,
      visibleIndices);

    var searchResultViewIndices = phantasus.MetadataUtil.search({
      model: metadata,
      fullModel: fullModel,
      text: searchText,
      isColumns: !isRows,
      matchAllPredicates: this.matchAllPredicates,
      defaultMatchMode: this.matchMode
    });
    if (searchText === '') {
      $searchResultsLabel.html('');
      if (isRows) {
        this.rowSearchObject.$searchResultsWrapper.hide();
      } else {
        this.columnSearchObject.$searchResultsWrapper.hide();
      }

    } else {
      $searchResultsLabel.html(searchResultViewIndices.length + ' match'
        + (searchResultViewIndices.length === 1 ? '' : 'es'));
      if (isRows) {
        this.rowSearchObject.$searchResultsWrapper.show();
      } else {
        this.columnSearchObject.$searchResultsWrapper.show();
      }

    }

    var searchResultsModelIndices = [];
    if (searchResultViewIndices != null) {
      for (var i = 0, length = searchResultViewIndices.length; i <
      length; i++) {
        var viewIndex = searchResultViewIndices[i];
        searchResultsModelIndices.push(isRows
          ? project.convertViewRowIndexToModel(viewIndex)
          : project.convertViewColumnIndexToModel(viewIndex));
      }
    }

    if (searchResultViewIndices !== null && isMatchesOnTop) {
      var key = new phantasus.MatchesOnTopSortKey(project,
        searchResultsModelIndices, 'matches on top', !isRows);
      // keep other sort keys
      searchResultViewIndices = key.indices; // matching indices
      // are now on top
      // add to beginning of sort keys
      sortKeys.splice(0, 0, key);
      if (isRows) {
        project.setRowSortKeys(sortKeys, false);
      } else {
        project.setColumnSortKeys(sortKeys, false);
      }
    }
    var searchResultsViewIndicesSet = new phantasus.Set();
    if (searchResultViewIndices != null) {
      for (var i = 0, length = searchResultViewIndices.length; i <
      length; i++) {
        var viewIndex = searchResultViewIndices[i];
        searchResultsViewIndicesSet.add(viewIndex);
      }
    }
    if (searchResultViewIndices == null) {
      searchResultViewIndices = [];
    }

    if (isRows) {
      this.rowSearchResultModelIndices = searchResultsModelIndices;
      this.rowSearchResultViewIndicesSorted = searchResultViewIndices.sort(
        function (a, b) {
          return a < b ? -1 : 1;
        });
      this.currentRowSearchIndex = -1;

    } else {
      this.columnSearchResultModelIndices = searchResultsModelIndices;
      this.columnSearchResultViewIndicesSorted = searchResultViewIndices.sort(
        function (a, b) {
          return a < b ? -1 : 1;
        });
      this.currentColumnSearchIndex = -1;
    }
    // update selection
    (!isRows
      ? project.getColumnSelectionModel()
      : project.getRowSelectionModel()).setViewIndices(
      searchResultsViewIndicesSet, true);

    if (isMatchesOnTop) { // resort
      if (isRows) {
        project.setRowSortKeys(phantasus.SortKey.keepExistingSortKeys(
          sortKeys, project.getRowSortKeys()), true);
      } else {
        project.setColumnSortKeys(
          phantasus.SortKey.keepExistingSortKeys(sortKeys,
            project.getColumnSortKeys()), true);
      }
    }
    this.updateDimensionsLabel();
    this.updateSelectionLabel();
    this.searching = false;

  },
  isSelectionOnTop: function (isColumns) {
    var $btn = isColumns
      ? this.columnSearchObject.$matchesToTop
      : this.rowSearchObject.$matchesToTop;
    return $btn.hasClass('btn-primary');
  },
  setSelectionOnTop: function (options) {
    if (options.updateButtonStatus) {
      var $btn = options.isColumns
        ? this.columnSearchObject.$matchesToTop
        : this.rowSearchObject.$matchesToTop;
      if (options.isOnTop) {
        $btn.addClass('btn-primary');
      } else {
        $btn.removeClass('btn-primary');
      }
    }
    var project = this.heatMap.getProject();
    var sortKeys = options.isColumns
      ? project.getColumnSortKeys()
      : project.getRowSortKeys();
    // remove existing matches on top key
    sortKeys = sortKeys.filter(function (key) {
      return !(key instanceof phantasus.MatchesOnTopSortKey &&
      key.name === 'matches on top');
    });
    if (options.isOnTop) { // bring to top
      var key = new phantasus.MatchesOnTopSortKey(project,
        options.isColumns
          ? this.columnSearchResultModelIndices
          : this.rowSearchResultModelIndices,
        'matches on top');
      sortKeys.splice(0, 0, key);
      if (options.isColumns) {
        this.heatMap.scrollLeft(0);
      } else {
        this.heatMap.scrollTop(0);
      }
    }
    this.searching = true;
    if (options.isColumns) {
      project.setColumnSortKeys(sortKeys, true);
    } else {
      project.setRowSortKeys(sortKeys, true);
    }
    this._updateSearchIndices(options.isColumns);
    this.searching = false;

  }
};
phantasus.HeatMapToolBar.COLUMN_SEARCH_FIELD = 'column';
phantasus.HeatMapToolBar.ROW_SEARCH_FIELD = 'column';
phantasus.HeatMapToolBar.COLUMN_DENDROGRAM_SEARCH_FIELD = 'column_dendrogram';
phantasus.HeatMapToolBar.ROW_DENDROGRAM_SEARCH_FIELD = 'row_dendrogram';

phantasus.HeatMapTooltipProvider = function (heatMap, rowIndex, columnIndex, options, separator, quick, tipText) {
  var dataset = heatMap.project.getSortedFilteredDataset();
  if (!quick) {
    if (options.value) { // key value pairs for custom tooltip
      _.each(options.value, function (pair) {
        if (tipText.length > 0) {
          tipText.push(separator);
        }
        tipText.push(pair.name);
        tipText.push(': <b>');
        if (_.isArray(pair.value)) {
          for (var i = 0; i < pair.value.length; i++) {
            if (i > 0) {
              tipText.push(', ');
            }
            tipText.push(pair.value[i]);
          }
        } else {
          tipText.push(pair.value);
        }
        tipText.push('</b>');
      });
    }
  }
  if (rowIndex !== -1 && columnIndex !== -1) {
    var tooltipSeriesIndices = options.tooltipSeriesIndices ? options.tooltipSeriesIndices : phantasus.Util.sequ32(dataset.getSeriesCount());
    for (var i = 0, nseries = tooltipSeriesIndices.length; i < nseries; i++) {
      phantasus.HeatMapTooltipProvider._matrixValueToString(heatMap, dataset,
        rowIndex, columnIndex, tooltipSeriesIndices[i], tipText, separator,
        options.showSeriesNameInTooltip || i > 0);
      if (heatMap.options.symmetric && dataset.getValue(rowIndex, columnIndex, tooltipSeriesIndices[i]) !== dataset.getValue(columnIndex, rowIndex, tooltipSeriesIndices[i])) {
        phantasus.HeatMapTooltipProvider._matrixValueToString(heatMap, dataset,
          columnIndex, rowIndex, tooltipSeriesIndices[i], tipText, separator, false);
      }
    }

    if (quick) {
      var quickRowTracks = heatMap.rowTracks.filter(function (t) {
        return t.settings.inlineTooltip;
      });
      phantasus.HeatMapTooltipProvider._tracksToString(quickRowTracks, dataset.getRowMetadata(), rowIndex, tipText, separator);
      phantasus.HeatMapTooltipProvider._tracksToString(heatMap.columnTracks.filter(function (t) {
        return t.settings.inlineTooltip;
      }), dataset.getColumnMetadata(), columnIndex, tipText, separator);

    }
  } else if (quick) {
    if (rowIndex !== -1) {
      phantasus.HeatMapTooltipProvider._tracksToString(heatMap.rowTracks.filter(function (t) {
        return t.settings.inlineTooltip && options.name !== t.getName();
      }), dataset.getRowMetadata(), rowIndex, tipText, separator);
    }
    if (columnIndex !== -1) {
      phantasus.HeatMapTooltipProvider._tracksToString(heatMap.columnTracks.filter(function (t) {
        return t.settings.inlineTooltip && options.name !== t.getName();
      }), dataset.getColumnMetadata(), columnIndex, tipText, separator);
    }
  }

  if (!quick) {
    if (rowIndex !== -1) {
      phantasus.HeatMapTooltipProvider._metadataToString(options,
        heatMap.rowTracks, dataset.getRowMetadata(), rowIndex,
        tipText, separator);
    }
    if (columnIndex !== -1) {
      phantasus.HeatMapTooltipProvider._metadataToString(options,
        heatMap.columnTracks, dataset.getColumnMetadata(),
        columnIndex, tipText, separator);
    }
  } else if (options.name != null) {
    var metadata = (rowIndex !== -1 ? dataset.getRowMetadata() : dataset.getColumnMetadata());
    var vector = metadata.getByName(options.name);
    var track = heatMap.getTrack(options.name, columnIndex !== -1);
    var colorByName = track != null ? track.settings.colorByField : null;
    var additionalVector = colorByName != null ? metadata
      .getByName(colorByName) : null;
    phantasus.HeatMapTooltipProvider.vectorToString(vector,
      rowIndex !== -1 ? rowIndex : columnIndex, tipText, separator,
      additionalVector);

  }
  var rowNodes = [];
  var columnNodes = [];
  var selectedRowNodes = [];
  var selectedColumnNodes = [];

  if (options.rowNodes) {
    rowNodes = options.rowNodes;
  }
  if (options.columnNodes) {
    columnNodes = options.columnNodes;
  }
  if (!quick) {
    if (heatMap.rowDendrogram) {
      selectedRowNodes = _
        .values(heatMap.rowDendrogram.selectedRootNodeIdToNode);
    }
    if (heatMap.columnDendrogram) {
      selectedColumnNodes = _
        .values(heatMap.columnDendrogram.selectedRootNodeIdToNode);
    }
    if (selectedRowNodes.length > 0 && rowNodes.length > 0) {
      var nodeIds = {};
      _.each(selectedRowNodes, function (n) {
        nodeIds[n.id] = true;
      });
      rowNodes = _.filter(rowNodes, function (n) {
        return nodeIds[n.id] === undefined;
      });
    }
    if (selectedColumnNodes.length > 0 && columnNodes.length > 0) {
      var nodeIds = {};
      _.each(selectedColumnNodes, function (n) {
        nodeIds[n.id] = true;
      });
      columnNodes = _.filter(columnNodes, function (n) {
        return nodeIds[n.id] === undefined;
      });
    }
  }
  phantasus.HeatMapTooltipProvider._nodesToString(tipText, rowNodes, null, separator);
  phantasus.HeatMapTooltipProvider._nodesToString(tipText, columnNodes, null, separator);
  if (!quick) {
    if (selectedRowNodes.length > 0) {
      phantasus.HeatMapTooltipProvider._nodesToString(tipText,
        selectedRowNodes, heatMap.rowDendrogram._selectedNodeColor,
        separator);
    }
    if (selectedColumnNodes.length > 0) {
      phantasus.HeatMapTooltipProvider._nodesToString(tipText,
        selectedColumnNodes,
        heatMap.columnDendrogram._selectedNodeColor, separator);
    }
  }

};

phantasus.HeatMapTooltipProvider._matrixValueToString = function (heatMap, dataset, rowIndex, columnIndex, seriesIndex, tipText, separator, showSeriesNameInTooltip) {
  var val = dataset.getValue(rowIndex, columnIndex, seriesIndex);
  if (val != null) {
    var nf = heatMap.getHeatMapElementComponent().getDrawValuesFormat();
    if (val.toObject || !_.isNumber(val)) {
      var obj = val.toObject ? val.toObject() : val;
      if (phantasus.Util.isArray(obj)) {
        var v = phantasus.Util.toString(obj);
        if (tipText.length > 0) {
          tipText.push(separator);
        }
        if (showSeriesNameInTooltip) {
          tipText.push(dataset.getName(seriesIndex));
          tipText.push(': ');
        }
        tipText.push('<b>');
        tipText.push(v);
        tipText.push('</b>');
      } else {
        var keys = _.keys(obj);
        if (keys.length === 0) {
          var v = phantasus.Util.toString(obj);
          if (tipText.length > 0) {
            tipText.push(separator);
          }
          if (showSeriesNameInTooltip) {
            tipText.push(dataset.getName(seriesIndex));
            tipText.push(': ');
          }
          tipText.push('<b>');
          tipText.push(v);
          tipText.push('</b>');
        } else {
          for (var i = 0, nkeys = keys.length; i < nkeys; i++) {
            var key = keys[i];
            if (key !== '__v') { // special value key
              var objVal = obj[key];
              var v;
              if (phantasus.Util.isArray(objVal)) {
                v = phantasus.Util.arrayToString(objVal, ', ');
              } else {
                v = phantasus.Util.toString(objVal);
              }
              if (tipText.length > 0) {
                tipText.push(separator);
              }
              tipText.push(key);
              tipText.push(': <b>');
              tipText.push(v);
              tipText.push('</b>');
            }
          }
          if (_.isNumber(val)) {
            tipText.push(separator);
            tipText.push('Value: <b>');
            tipText.push(nf(val));
            tipText.push('</b>');
          }
        }
      }
    } else {
      if (tipText.length > 0) {
        tipText.push(separator);
      }

      if (showSeriesNameInTooltip) {
        tipText.push(dataset.getName(seriesIndex));
        tipText.push(': ');
      }
      tipText.push('<b>');
      tipText.push(nf(val));
      tipText.push('</b>');
    }
  }
};

phantasus.HeatMapTooltipProvider.vectorToString = function (vector, index, tipText, separator, additionalVector) {
  var arrayValueToString = function (arrayFieldName, arrayVal) {
    if (arrayVal != null) {
      if (arrayFieldName != null) {
        if (tipText.length > 0) {
          tipText.push(separator);
        }
        tipText.push(arrayFieldName); // e.g. PC3
      }
      if (arrayVal.toObject) {
        tipText.push(' ');
        var obj = arrayVal.toObject();
        var keys = _.keys(obj);
        _.each(keys, function (key) {
          var subVal = obj[key];
          if (subVal != null && subVal != '') {
            if (tipText.length > 0) {
              tipText.push(separator);
            }
            tipText.push(key);
            tipText.push(': <b>');
            tipText.push(phantasus.Util.toString(subVal));
            tipText.push('</b>');
          }
        });
      } else {
        tipText.push(': <b>');
        tipText.push(phantasus.Util.toString(arrayVal));
        tipText.push('</b>');
      }

    }
  };
  if (vector != null) {
    var primaryVal = vector.getValue(index);
    if (primaryVal != null && primaryVal != '') {
      var primaryFields = vector.getProperties().get(
        phantasus.VectorKeys.FIELDS);
      if (primaryFields != null) {
        var visibleFieldIndices = vector.getProperties().get(
          phantasus.VectorKeys.VISIBLE_FIELDS);
        if (visibleFieldIndices === undefined) {
          visibleFieldIndices = phantasus.Util
            .seq(primaryFields.length);
        }
        var additionalFieldNames = additionalVector != null ? additionalVector
          .getProperties().get(phantasus.VectorKeys.FIELDS)
          : null;
        var additionalVal = additionalFieldNames != null ? additionalVector
          .getValue(index)
          : null;
        if (tipText.length > 0) {
          tipText.push(separator);
        }
        tipText.push(vector.getName());
        for (var j = 0; j < visibleFieldIndices.length; j++) {
          arrayValueToString(primaryFields[visibleFieldIndices[j]],
            primaryVal[visibleFieldIndices[j]]);
        }

        if (additionalVal != null) {
          if (tipText.length > 0) {
            tipText.push(separator);
          }
          tipText.push(additionalVector.getName());
          for (var j = 0; j < visibleFieldIndices.length; j++) {
            arrayValueToString(
              additionalFieldNames[visibleFieldIndices[j]],
              additionalVal[visibleFieldIndices[j]]);
          }

        }
      } else if (primaryVal.summary) {
        if (tipText.length > 0) {
          tipText.push(separator);
        }
        tipText.push(vector.getName());
        tipText.push(': ');
        var obj = primaryVal.summary;
        var keys = _.keys(obj);
        for (var i = 0, nkeys = keys.length; i < nkeys; i++) {
          var key = keys[i];
          if (key !== '__v') { // special value key
            var objVal = obj[key];
            var v;
            if (phantasus.Util.isArray(objVal)) {
              v = phantasus.Util.arrayToString(objVal, ', ');
            } else {
              v = phantasus.Util.toString(objVal);
            }
            if (tipText.length > 0) {
              tipText.push(separator);
            }
            tipText.push(key);
            tipText.push(': <b>');
            tipText.push(v);
            tipText.push('</b>');
          }
        }

      } else {
        if (tipText.length > 0) {
          tipText.push(separator);
        }
        tipText.push(vector.getName());
        tipText.push(': <b>');
        tipText.push(phantasus.Util.toString(primaryVal));
        tipText.push('</b>');
      }

    }
  }
};
phantasus.HeatMapTooltipProvider._tracksToString = function (tracks, metadata, index, tipText, separator) {
  for (var i = 0; i < tracks.length; i++) {
    phantasus.HeatMapTooltipProvider.vectorToString(metadata.getByName(tracks[i].name), index, tipText,
      separator);

  }
};
phantasus.HeatMapTooltipProvider._metadataToString = function (options, tracks, metadata, index,
                                                              tipText, separator) {
  var filtered = [];
  for (var i = 0, ntracks = tracks.length; i < ntracks; i++) {
    var track = tracks[i];
    if ((track.isVisible() && track.isShowTooltip())) {
      if (tracks[i].name === options.name) { // show the vector that we're mousing over 1st
        filtered.splice(0, 0, track);
      } else {
        filtered.push(track);
      }
    }
  }

  phantasus.HeatMapTooltipProvider._tracksToString(filtered, metadata, index, tipText, separator);

};
phantasus.HeatMapTooltipProvider._nodesToString = function (tipText, nodes, color, separator) {
  var renderField = function (name, value) {
    if (value != null) {
      if (tipText.length > 0) {
        tipText.push(separator);
      }
      if (color) {
        tipText.push('<span style="color:' + color + '">');
      }
      tipText.push(name);
      tipText.push(': <b>');
      if (_.isArray(value)) {
        for (var i = 0; i < value.length; i++) {
          if (i > 0) {
            tipText.push(', ');
          }
          tipText.push(phantasus.Util.toString(value[i]));
        }
      } else {
        tipText.push(phantasus.Util.toString(value));
      }
      tipText.push('</b>');
      if (color) {
        tipText.push('</span>');
      }
    }
  };
  _.each(nodes, function (node) {
    if (node.info) {
      for (var name in node.info) {
        var value = node.info[name];
        renderField(name, value);
      }
    }
    renderField('height', node.height);
    renderField('depth', node.depth);
    var nLeafNodes = 1 + Math.abs(node.maxIndex - node.minIndex);
    if (nLeafNodes > 0) {
      renderField('# of leaf nodes', nLeafNodes);
      // renderField('height', node.height);
    }
  });
};

phantasus.HeatMapTrackColorLegend = function (tracks, colorModel) {
  phantasus.AbstractCanvas.call(this, false);
  this.tracks = tracks;
  this.colorModel = colorModel;
  this.canvas.style.position = '';
};
phantasus.HeatMapTrackColorLegend.prototype = {
  getPreferredSize: function () {
    var tracks = this.tracks;
    var colorModel = this.colorModel;
    var xpix = 0;
    var ypix = 0;
    var maxYPix = 0;
    var canvas = this.canvas;
    var context = canvas.getContext('2d');
    context.font = '12px ' + phantasus.CanvasUtil.getFontFamily(context);
    for (var i = 0; i < tracks.length; i++) {
      ypix = 0;
      var maxWidth = 0;
      var vector = tracks[i].getVector(tracks[i].settings.colorByField);
      if (vector.getProperties().get(phantasus.VectorKeys.DISCRETE)) {
        var map = colorModel.getDiscreteColorScheme(vector);
        map.forEach(function (color, key) {
          var width = context.measureText(key).width;
          if (!isNaN(width)) {
            maxWidth = Math.max(maxWidth, width);
          }
          ypix += 14;
        });
      } else {
        maxWidth = 220;
        ypix += 40;

      }
      maxWidth = Math.max(maxWidth,
        context.measureText(vector.getName()).width);
      xpix += maxWidth + 10 + 14;
      maxYPix = Math.max(maxYPix, ypix);
    }
    return {
      width: xpix,
      height: maxYPix > 0 ? (maxYPix + 12) : 0
    };
  },
  draw: function (clip, context) {
    var tracks = this.tracks;
    var colorModel = this.colorModel;
    var xpix = 0;
    // legends are placed side by side
    for (var i = 0; i < tracks.length; i++) {
      var ypix = 0;
      var vector = tracks[i].getVector(tracks[i].settings.colorByField);
      context.fillStyle = phantasus.CanvasUtil.FONT_COLOR;
      context.font = '12px ' + phantasus.CanvasUtil.getFontFamily(context);
      context.textAlign = 'left';
      // draw name
      context.textBaseline = 'top';
      context.fillText(vector.getName(), xpix, ypix);

      context.strokeStyle = 'LightGrey';
      var maxWidth = 0;
      var textWidth = context.measureText(vector.getName()).width;
      if (!isNaN(textWidth)) {
        maxWidth = Math.max(0, textWidth);
      }
      ypix += 14;
      if (vector.getProperties().get(phantasus.VectorKeys.DISCRETE)) {
        var toStringFunction = phantasus.VectorTrack.vectorToString(vector);
        var map = colorModel.getDiscreteColorScheme(vector);
        var values = map.keys().sort(phantasus.SortKey.ASCENDING_COMPARATOR);
        values.forEach(function (key) {
          if (key != null) {
            key = toStringFunction(key);
            var color = colorModel.getMappedValue(vector, key);
            var textWidth = context.measureText(key).width;
            if (!isNaN(textWidth)) {
              maxWidth = Math.max(maxWidth, textWidth);
            }
            context.fillStyle = color;
            var xoffset = 0;
            if (tracks[i].isRenderAs(phantasus.VectorTrack.RENDER.COLOR)) {
              context.fillRect(xpix, ypix, 12, 12);
              context.strokeRect(xpix, ypix, 12, 12);
              context.fillStyle = phantasus.CanvasUtil.FONT_COLOR;
              xoffset = 16;
            }
            context.fillText(key, xpix + xoffset, ypix);
            ypix += 14;
          }
        });
      } else {
        var scheme = colorModel.getContinuousColorScheme(vector);
        context.save();
        context.translate(xpix, ypix);
        phantasus.HeatMapColorSchemeLegend.drawColorScheme(context,
          scheme, 200);
        context.restore();
        maxWidth = Math.max(maxWidth, 220);
        ypix += 40;
      }
      xpix += maxWidth + 10 + 14; // space between tracks + color chip
    }
  }
};
phantasus.Util.extend(phantasus.HeatMapTrackColorLegend, phantasus.AbstractCanvas);

phantasus.HeatMapTrackFontLegend = function (tracks, model) {
  phantasus.AbstractCanvas.call(this, false);
  this.tracks = tracks;
  this.model = model;
  this.canvas.style.position = '';
};
phantasus.HeatMapTrackFontLegend.prototype = {
  getPreferredSize: function () {
    var tracks = this.tracks;
    var model = this.model;
    var canvas = this.canvas;
    var context = canvas.getContext('2d');
    context.font = '900 12px ' + phantasus.CanvasUtil.getFontFamily(context);
    var xpix = 0;
    var ypix = 0;
    var maxYPix = 0;
    for (var i = 0; i < tracks.length; i++) {
      ypix = 0;
      var maxWidth = 0;
      var vector = tracks[i].getVector(tracks[i].settings.fontField);
      var map = model.getMap(vector.getName());

      map.forEach(function (font, key) {
        // skip normal font weight
        if (font != null && font.weight != '400') {
          var width = context.measureText(key).width;
          if (!isNaN(width)) {
            maxWidth = Math.max(maxWidth, width);
          }
          ypix += 14;
        }
      });

      xpix += maxWidth + 6;
      maxYPix = Math.max(maxYPix, ypix);
    }
    return {
      width: xpix,
      height: maxYPix > 0 ? (maxYPix + 30) : 0
    };
  },
  draw: function (clip, context) {
    // draw legends horizontally
    var tracks = this.tracks;
    var model = this.model;
    var xpix = 0;
    var ypix = 0;
    context.textAlign = 'left';
    context.textBaseline = 'top';
    context.font = '12px ' + phantasus.CanvasUtil.getFontFamily(context);
    context.fillStyle = phantasus.CanvasUtil.FONT_COLOR;
    context.strokeStyle = 'black';
    var font = context.font;
    for (var i = 0; i < tracks.length; i++) {
      ypix = 0;
      var maxWidth = 0;
      var textVector = tracks[i].getVector();
      var fontVector = tracks[i].getVector(tracks[i].settings.fontField);
      context.font = font;
      context.fillText(textVector.getName(), xpix, ypix); // vector name
      maxWidth = Math.max(maxWidth,
        context.measureText(textVector.getName()).width);
      ypix += 14;
      var map = model.getMap(fontVector.getName());
      var values = map.keys().sort(phantasus.SortKey.ASCENDING_COMPARATOR);
      values.forEach(function (key) {
        var mappedFont = model.getMappedValue(fontVector, key);
        if (mappedFont != null && mappedFont.weight != '400') {
          context.font = mappedFont.weight + ' ' + font;
          var width = context.measureText(key).width;
          if (!isNaN(width)) {
            maxWidth = Math.max(maxWidth, width);
          }
          context.fillText(key, xpix, ypix);
          ypix += 14;
        }
      });

      xpix += maxWidth + 6; // space between tracks
    }
  }
};
phantasus.Util.extend(phantasus.HeatMapTrackFontLegend, phantasus.AbstractCanvas);

phantasus.HeatMapTrackShapeLegend = function (tracks, shapeModel) {
  phantasus.AbstractCanvas.call(this, false);
  this.tracks = tracks;
  this.shapeModel = shapeModel;
  this.canvas.style.position = '';
};
phantasus.HeatMapTrackShapeLegend.prototype = {
  getPreferredSize: function () {
    var tracks = this.tracks;
    var shapeModel = this.shapeModel;
    var canvas = this.canvas;
    var context = canvas.getContext('2d');
    var xpix = 0;
    var ypix = 0;
    var maxYPix = 0;
    for (var i = 0; i < tracks.length; i++) {
      ypix = 0;
      var maxWidth = 0;
      var vector = tracks[i].getVector();
      var map = shapeModel.getMap(vector.getName());

      map.forEach(function (color, key) {
        var width = context.measureText(key).width;
        if (!isNaN(width)) {
          maxWidth = Math.max(maxWidth, width);
        }
        ypix += 14;
      });

      xpix += maxWidth + 24;
      maxYPix = Math.max(maxYPix, ypix);
    }
    return {
      width: xpix,
      height: maxYPix > 0 ? (maxYPix + 30) : 0
    };
  },
  draw: function (clip, context) {
    // draw legends horizontally
    var tracks = this.tracks;
    var shapeModel = this.shapeModel;
    var xpix = 0;
    var ypix = 0;
    context.textAlign = 'left';
    context.textBaseline = 'top';
    context.font = '12px ' + phantasus.CanvasUtil.getFontFamily(context);
    context.fillStyle = phantasus.CanvasUtil.FONT_COLOR;
    context.strokeStyle = 'black';
    for (var i = 0; i < tracks.length; i++) {
      ypix = 0;
      var maxWidth = 0;
      var vector = tracks[i].getVector();
      context.fillText(vector.getName(), xpix, ypix);
      maxWidth = Math.max(maxWidth,
        context.measureText(vector.getName()).width);
      ypix += 14;
      var map = shapeModel.getMap(vector.getName());
      var values = map.keys().sort(phantasus.SortKey.ASCENDING_COMPARATOR);
      values.forEach(function (key) {
        var shape = shapeModel.getMappedValue(vector, key);
        var width = context.measureText(key).width;
        if (!isNaN(width)) {
          maxWidth = Math.max(maxWidth, width);
        }
        phantasus.CanvasUtil.drawShape(context, shape, xpix + 8,
          ypix + 6, 6);
        context.fillText(key, xpix + 16, ypix);
        ypix += 14;
      });

      xpix += maxWidth + 24; // space between columns + shape
    }
  }
};
phantasus.Util.extend(phantasus.HeatMapTrackShapeLegend, phantasus.AbstractCanvas);

/**
 * rows and columns can contain field, renameTo, display, order
 *
 */

phantasus.HeatMap = function (options) {
  phantasus.Util.loadTrackingCode();
  var _this = this;
  // don't extend
  var dontExtend = ['parent', 'columnDendrogram', 'rowDendrogram'];
  var cache = [];
  for (var i = 0; i < dontExtend.length; i++) {
    var field = dontExtend[i];
    cache[i] = options[field];
    options[field] = null;
  }
  options = $
    .extend(
      true,
      {},
      {
        /*
         * The element in which to render to the heat map.
         */
        el: null,
        /*
         * A File or URL to a <a target="_blank"
         * href="http://support.lincscloud.org/hc/en-us/articles/202105453-GCT-Gene-Cluster-Text-Format-">GCT
         * 1.3</a>, ' + '<a target="_blank"
         * href="http://www.broadinstitute.org/cancer/software/genepattern/gp_guides/file-formats/sections/gct">GCT
         * 1.2</a>, ' + '<a target="_blank"
         * href="https://wiki.nci.nih.gov/display/TCGA/Mutation+Annotation+Format+%28MAF%29+Specification">MAF</a>, ' + '<a
         * target="_blank",
         * href="http://www.broadinstitute.org/cancer/software/gsea/wiki/index.php/Data_formats#GMT:_Gene_Matrix_Transposed_file_format_.28.2A.gmt.29">GMT</a>, ' + '
         * or a tab-delimitted text file. Can also be an array
         * of File or URLs in which case the datasets are
         * combined by matching on column ids.
         */
        dataset: undefined,
        /*
         *
         * @description Array of file, datasetField, fileField,
         *              and include (optional fields to include
         *              from file). File can be xlsx file,
         *              tab-delimitted text file, or gmt file.
         *              <p>
         *              <b>Example:</b> Annotate rows matching
         *              'name' field in dataset to 'id' field in
         *              file.
         *              </p>
         *              <code>[{file:'https://MY_URL', datasetField:'name', fileField:'id'}]</code>
         */
        rowAnnotations: undefined,
        /*
         * Array of file, datasetField, fileField, and include
         * (optional fields to include from file). File can be
         * xlsx file, tab-delimitted text file, or gmt file.
         */
        columnAnnotations: undefined,

      /*
       * Array of column metadata names to group the heat map
       * by.
       *
       * <p>
       * <b>Example:</b> Group by the type and gender
       * metadata field.
       * </p>
       *
       * <code>['type', 'gender']</code>
       */
      columnGroupBy: undefined,
      /*
       * Array of row metadata names to group the heat map by.
       *
       * <p>
       * <b>Example:</b> Group by the gene metadata field.
       * </p>
       * <code>['gene']</code>
       */
      rowGroupBy: undefined,
      /*
       * Object that describes mapping of values to colors.
       * scalingMode can be 'fixed' or 'relative'. Stepped indicates
       * whether color scheme is continuous (false) or
       * discrete (true).
       * <p>
       * <b>Example:</b> Use a fixed color scheme with color
       * stops at -100, -90, 90, and 100.
       * <p>
       * <code>{ scalingMode : 'fixed', stepped:false, values : [-100, -90, 90, 100], colors : ['blue', 'white', 'white', 'red'] };</code>
       */
      colorScheme: undefined,
      /*
       * Array of metadata names and sort order. Use 0 for
       * ascending and 1 for descending. To sort by values use
       * modelIndices.
       *
       * <p>
       * <b>Example:</b> Sort ascending by gene, and then
       * descending by stdev
       * </p>
       * <code>[{field:'gene', order:0}, {field:'stdev',
       *              order:1}]</code>
       */
      rowSortBy: undefined,
      /*
       * Array of metadata names and sort order. Use 0 for
       * ascending and 1 for descending.
       *
       * <p>
       * <b>Example:</b> to sort ascending by gene, and then
       * descending by stdev
       * </p>
       * <code> [{name:'gene',
       *              order:0}, {name:'stdev', order:1}]</code>
       */
      columnSortBy: undefined,
      /*
       * URL to a dendrogram in <a target="_blank"
       * href="https://en.wikipedia.org/wiki/Newick_format">Newick
       * format</a>
       */
      rowDendrogram: undefined,
      /*
       * URL to a dendrogram in <a target="_blank"
       * href="https://en.wikipedia.org/wiki/Newick_format">Newick
       * format</a>
       */
      columnDendrogram: undefined,

      /*
       * Column metadata field in dataset used to match leaf
       * node ids in column dendrogram Newick file
       */
      columnDendrogramField: 'id',
      /*
       * Row metadata field in dataset used to match leaf node
       * ids in row dendrogram Newick file
       */
      rowDendrogramField: 'id',
      /*
       * Array of objects describing how to display row
       * metadata fields. Each object in the array must have
       * field, and optionally display, order, and renameTo.
       * Field is the metadata field name. Display is a comma
       * delimited string that describes how to display a
       * metadata field. Options are text, color, stacked_bar,
       * bar, highlight, shape, discrete, and continuous.
       * Order is a number that indicates the order in which
       * the field should appear in the heat map. RenameTo
       * allows you to rename a field.
       */
      rows: [],
      /*
       * Array of objects describing how to display column
       * metadata fields. Each object in the array must have
       * field, and optionally display, order, and renameTo.
       * Field is the metadata field name. Display is a comma
       * delimited string that describes how to display a
       * metadata field. Options are text, color, stacked_bar,
       * bar, highlight, shape, discrete, and continuous.
       * Order is a number that indicates the order in which
       * the field should appear in the heat map. RenameTo
       * allows you to rename a field.
       */
      columns: [],
      /*
       * Optional array of tools to run at load time. For
       * example: <code>tools : [ {
       * name : 'Marker Selection',
       * params : {
       * 		field : [ comparisonVector.getName() ],
       *      class_a : [ 'A' ], class_b : [ 'B' ] }} ]</code>
       */
      tools: undefined,
      /*
       * Optional array of {name:string, values:[]}
       */
      rowFilter: undefined,
      columnFilter: undefined,
      /*
       * Whether to auto-hide the tab bar when only one tab is visible
       */
      autohideTabBar: false,
      /*
       * Whether this heat map tab can be closed
       */
      closeable: true,
      /*
       * Whether heat map tab can be renamed
       */
      rename: true,
      /*
       * Heat map row size in pixels or 'fit' to fit heat map to current height.
       */
      rowSize: 13,
      /*
       * Heat map column size in pixels or 'fit' to fit heat map to current width.
       */
      columnSize: 13,
      rowGapSize: 10,
      columnGapSize: 10,
      /*
       * Whether to draw heat map grid
       */
      drawGrid: true,
      /*
       * Heat map grid color
       */
      gridColor: '#808080',

      showRowNumber: false,
      /*
       * Heat map grid thickness in pixels
       */
      gridThickness: 0.1,
      height: 'window', // set the available height for the
      // heat map. If not
      // set, it will be determined automatically
      width: undefined, // set the available width for the
      // heat map. If not
      // set, it will be determined automatically
      /* Whether to focus this tab */
      focus: true,
      tooltipMode: 0, // 0=top status bar, 1=dialog, 2=follow
      inheritFromParent: true,
      inheritFromParentOptions: {
        transpose: false
      },
      /** Callback function to invoke for customizing inline matrix tooltips. */
      tooltip: undefined,
      structureUrlProvider: undefined,
      promises: undefined, // additional promises to wait
      // for
      // not inherited
      renderReady: undefined,
      // not inherited
      datasetReady: undefined,
      // inherited
      tabOpened: undefined,
      loadedCallback: undefined,
      name: undefined,
      rowsSortable: true,
      columnsSortable: true,
      popupEnabled: true,
      symmetric: false,
      keyboard: true,
      inlineTooltip: true,
      // Prevent mousewheel default (stops accidental page back on Mac), but also prevents page
      // scrolling
      standalone: false,
      $loadingImage: phantasus.Util.createLoadingEl(),
      menu: {
        File: [
          'Open', null, 'Save Image', 'Save Dataset', 'Save Session', null, 'Close Tab', null, 'Rename' +
          ' Tab'],
        Tools: [
          'New Heat Map',
          null,
          'Hierarchical Clustering',
          null,
          'Marker Selection',
          'Nearest Neighbors',
          'Create Calculated Annotation',
          null,
          'Adjust',
          'Collapse',
          'Similarity Matrix',
          'Transpose',
          null,
          'Chart',
          null,
          'Sort/Group',
          'Filter',
          null,
          'API',
          null,
          'k-means',
          'limma',
          'PCA Plot',
          'Submit to Enrichr',
          'Submit to Shiny GAM',
          'GSEA plot'],
        View: ['Zoom In', 'Zoom Out', null, 'Fit To Window', 'Fit Rows To Window', 'Fit Columns To Window', null, '100%', null, 'Options'],
        Edit: [
          'Copy Image',
          'Copy Selected Dataset',
          null,
          'Move Selected Rows To Top',
          'Annotate Selected Rows',
          'Copy Selected Rows',
          'Invert' +
          ' Selected Rows',
          'Select All Rows',
          'Clear Selected Rows',
          null,
          'Move Selected Columns To Top',
          'Annotate Selected Columns',
          'Copy Selected Columns',
          'Invert' +
          ' Selected Columns',
          'Select All Columns',
          'Clear Selected Columns'],
        Help: [
          'Search Menus', null, 'Contact', 'Configuration', 'Tutorial', 'Source Code', null, 'Keyboard' +
          ' Shortcuts']
      },
      toolbar: {
        dimensions: true,
        zoom: true,
        searchRows: true,
        searchColumns: true,
        searchValues: false,
        options: true,
        saveImage: true,
        filter: true,
        colorKey: true
      }
    }, options);

  for (var i = 0; i < dontExtend.length; i++) {
    var field = dontExtend[i];
    options[field] = cache[i];
  }
  if (options.menu == null) {
    options.menu = {};
  }
  if (options.toolbar == null) {
    options.toolbar = {};
  }

  this.options = options;
  this.tooltipProvider = phantasus.HeatMapTooltipProvider;
  if (!options.el) {
    this.$el = $('<div></div>');
  } else {
    this.$el = $(options.el);
  }
  this.rowGapSize = options.rowGapSize;
  this.columnGapSize = options.columnGapSize;
  this.actionManager = new phantasus.ActionManager();
  this.actionManager.heatMap = this;
  this.$el.addClass('phantasus');

  if (this.options.dataset == null) {
    var datasetFormBuilder = new phantasus.FormBuilder();
    datasetFormBuilder.append({
      name: 'file',
      type: 'file'
    });
    this.options.dataset = $.Deferred();
    phantasus.FormBuilder.showOkCancel({
      title: 'Dataset',
      appendTo: this.getContentEl(),
      focus: this.getFocusEl(),
      content: datasetFormBuilder.$form,
      okCallback: function () {
        var file = datasetFormBuilder.getValue('file');
        phantasus.DatasetUtil.read(file).done(function (dataset) {
          // console.log('now resolving here?');
          if (dataset.length && dataset.length > 0) {
            _this.options.dataset.resolve(dataset[0]);
            _this.setName(dataset[0].seriesNames[0]);
            for (var i = 1; i < dataset.length; i++) {
              var heatmap = new phantasus.HeatMap({
                name: dataset[i].seriesNames[0],
                dataset: dataset[i],
                parent: _this.heatmap,
                symmetric: _this.options.symmetric,
                inheritFromParent: false
              });
              // console.log(i, dataset[i], heatmap);
            }
          }
          else {
            _this.options.dataset.resolve(dataset);
          }
        }).fail(function (err) {
          _this.options.dataset.reject(err);
        });
      },
      cancelCallback: function () {
        _this.options.dataset.reject('Session cancelled.');
      }
    });
  }
  if (this.options.name == null) {
    this.options.name = (this.options.dataset.options && this.options.dataset.options.exactName) ? this.options.dataset.options.exactName :
      phantasus.Util
      .getBaseFileName(phantasus.Util
        .getFileName(this.options.dataset.file ? this.options.dataset.file
          : this.options.dataset));

  }

  var isPrimary = this.options.parent == null;
  if (this.options.parent == null) {

    if (!phantasus.Util.isHeadless()) {
      if (this.options.tabManager == null) {
        this.tabManager =
          new phantasus.TabManager({
            landingPage: function () {
              if (_this.options.landingPage == null) {
                _this.options.landingPage = new phantasus.LandingPage({tabManager: _this.tabManager});
                _this.options.landingPage.$el.prependTo(_this.$el);
              }
              return _this.options.landingPage;
            },
            autohideTabBar: this.options.autohideTabBar
          });
      } else {
        this.tabManager = this.options.tabManager;
      }

      if (this.options.tabManager == null) {
        this.tabManager.appendTo(this.$el);
      }
    }
  } else {
    if (this.options.inheritFromParent) {
      this.popupItems = this.options.parent.popupItems;
      if (!this.options.tabOpened) {
        this.options.tabOpened = this.options.parent.options.tabOpened;
      }
      this.options.drawCallback = this.options.parent.options.drawCallback;
    }
    this.tabManager = this.options.parent.tabManager;
  }
  this.$content = $('<div></div>');
  this.$content.css({
    'width': '100%',
    'user-select': 'none',

    '-webkit-user-select': 'none',
    '-webkit-user-drag': 'none',
    '-webkit-tap-highlight-color': 'rgba(0, 0, 0, 0)',

    '-moz-user-select': 'none',
    '-moz-user-drag': 'none',
    '-moz-tap-highlight-color': 'rgba(0, 0, 0, 0)',

    '-ms-user-select': 'none',
    '-ms-user-drag': 'none',
    '-ms-tap-highlight-color': 'rgba(0, 0, 0, 0)',

    '-o-user-select': 'none',
    '-o-user-drag': 'none',
    '-o-tap-highlight-color': 'rgba(0, 0, 0, 0)',

    'overflow-x': 'visible',
    'overflow-y': 'visible'
  });

  this.$content.on('remove.phantasus', function () {
    _this.$content.off('remove.phantasus');
    _this.dispose();
  });
  if (!phantasus.Util.isHeadless()) {
    var tab = this.tabManager.add({
      $el: this.$content,
      closeable: this.options.closeable,
      rename: this.options.rename,
      title: this.options.name,
      object: this,
      focus: this.options.focus
    });
    this.tabId = tab.id;
    this.$tabPanel = tab.$panel;
  }
  if (options.$loadingImage) {
    options.$loadingImage.appendTo(this.$content);
  }

  this.options.dataSource = !options.dataset ? ''
    : (options.dataset.file ? options.dataset.file : options.dataset);
  this._togglingInfoWindow = false;

  var promises = [];
  if (options.promises) {
    for (var i = 0; i < options.promises.length; i++) {
      promises.push(options.promises[i]);
    }
  }
  this.whenLoaded = [];

  if (options.rowAnnotations) {
    var rowDef = phantasus.DatasetUtil.annotate({
      annotations: options.rowAnnotations,
      isColumns: false
    });
    rowDef.done(function (callbacks) {
      _this.whenLoaded = _this.whenLoaded.concat(callbacks);
    });
    promises.push(rowDef);

  }
  //// console.log("HeatMap creation ::", options.columnAnnotations);
  if (options.columnAnnotations) {
    var columnDef = phantasus.DatasetUtil.annotate({
      annotations: options.columnAnnotations,
      isColumns: true
    });
    columnDef.done(function (callbacks) {
      _this.whenLoaded = _this.whenLoaded.concat(callbacks);
    });
    promises.push(columnDef);
  }

  if (options.rowDendrogram != null
    && _.isString(options.rowDendrogram)) {
    if (options.rowDendrogram[0] === '(') {
      _this.options.rowDendrogram = phantasus.DendrogramUtil
        .parseNewick(options.rowDendrogram);
    } else {
      var rowDendrogramDeferred = phantasus.Util
        .getText(options.rowDendrogram);
      rowDendrogramDeferred.done(function (text) {
        _this.options.rowDendrogram = phantasus.DendrogramUtil
          .parseNewick(text);
      });
      promises.push(rowDendrogramDeferred);
    }

  }
  if (options.columnDendrogram != null
    && _.isString(options.columnDendrogram)) {
    if (options.columnDendrogram[0] === '(') {
      _this.options.columnDendrogram = phantasus.DendrogramUtil.parseNewick(options.columnDendrogram);
    } else {
      var columnDendrogramDeferred = phantasus.Util.getText(options.columnDendrogram);
      columnDendrogramDeferred.done(function (text) {
        _this.options.columnDendrogram = phantasus.DendrogramUtil.parseNewick(text);
      });
      promises.push(columnDendrogramDeferred);
    }

  }
  var heatMapLoaded = function () {
    // console.log('heatMapLoaded', _this.options.name);
    phantasus.DatasetUtil.toESSessionPromise(options.dataset);
    if (typeof window !== 'undefined') {
      $(window).on('orientationchange.phantasus resize.phantasus', _this.resizeListener = function () {
        _this.revalidate();
      });
    }
    _this.revalidate();
    if (options.loadedCallback) {
      options.loadedCallback(_this);
    }

    if (_this.tabManager) {
      if (_this.options.focus) {
        _this.tabManager.setActiveTab(tab.id);
        _this.focus();
      } else if (_this.tabManager.getTabCount() === 1) {
        _this.tabManager.setActiveTab(tab.id);
      }
    }
    _this.$el.trigger('heatMapLoaded', _this);
  };
  if (phantasus.Util.isArray(options.dataset)) {
    var d = phantasus.DatasetUtil.readDatasetArray(options.dataset);
    d.fail(function (message) {
      if (_this.options.$loadingImage) {
        _this.options.$loadingImage.remove();
      }
      if (_this.options.error) {
        _this.options.error(message);
      }
      phantasus.FormBuilder.showInModal({
        title: 'Error',
        html: message,
        appendTo: _this.getContentEl(),
        focus: _this.getFocusEl()
      });
    });
    d
    .done(function (joined) {
      if (_this.options.$loadingImage) {
        _this.options.$loadingImage.remove();
      }

      _this.options.dataset = joined;
      _this._init();
      if (joined.getRowMetadata().getByName('Source') != null
        && !_this.options.colorScheme) {
        _this.heatmap.getColorScheme().setSeparateColorSchemeForRowMetadataField(
          'Source');
      }

      _.each(
        options.dataset,
        function (option) {
          if (option.colorScheme) {
            _this.heatmap.getColorScheme().setCurrentValue(
              phantasus.Util.getBaseFileName(phantasus.Util.getFileName(option.dataset)));
            _this.heatmap.getColorScheme().setColorSupplierForCurrentValue(
              phantasus.AbstractColorSupplier.fromJSON(option.colorScheme));

          } else {
            try {
              _this.autoDisplay({
                extension: phantasus.Util.getExtension(phantasus.Util.getFileName(option.dataset)),
                filename: phantasus.Util.getBaseFileName(phantasus.Util.getFileName(option.dataset))
              });
            }
            catch (x) {
              console.log('Autodisplay errror');
            }

              }
            });

        heatMapLoaded();
      });
  } else {
    var deferred = options.dataset.file ? phantasus.DatasetUtil.read(
      options.dataset.file, options.dataset.options)
      : phantasus.DatasetUtil.read(options.dataset);
    deferred.done(function (dataset) {
      // console.log('resolving here?', dataset);

      if (dataset.length && dataset.length > 0) {
        _this.options.dataset = dataset[0];
        _this.setName(dataset[0].seriesNames[0]);

        for (var i = 1; i < dataset.length; i++) {

          var heatmap = new phantasus.HeatMap({
            name: dataset[i].seriesNames[0],
            dataset: dataset[i],
            symmetric: _this.options.symmetric,
            inheritFromParent: false
          });
          console.log(i, dataset[i], heatmap);
        }
      }
      else {
        _this.options.dataset = dataset;
      }
    });
    deferred.fail(function (err) {
      _this.options.$loadingImage.remove();
      var message = [
        'Error opening '
        + (options.dataset.file ? phantasus.Util.getFileName(options.dataset.file) : phantasus.Util.getFileName(options.dataset)) + '.'];

      if (err.message) {
        message.push('<br />Cause: ');
        message.push(err.message);

      }
      if (_this.options.error) {
        _this.options.error(message);
      }
      phantasus.FormBuilder.showInModal({
        title: 'Error',
        html: message.join(''),
        appendTo: _this.getContentEl(),
        focus: _this.getFocusEl()
      });
    });

    promises.push(deferred);
    var datasetOverlay = null;
    if (options.datasetOverlay) {
      var d = options.datasetOverlay.file ? phantasus.DatasetUtil.read(
        options.datasetOverlay.file, options.datasetOverlay.options)
        : phantasus.DatasetUtil.read(options.datasetOverlay);
      d.done(function (dataset) {
        datasetOverlay = dataset;
      });
      promises.push(d);
    }
    $.when.apply($, promises).done(function () {

      if (_this.options.$loadingImage) {
        _this.options.$loadingImage.remove();
      }
      if (_this.options.dataset == null) {
        return _this.tabManager.remove(_this.tabId);
      }
      _this._init();
      if (datasetOverlay) {
        phantasus.DatasetUtil.overlay({
          dataset: _this.options.dataset,
          newDataset: datasetOverlay,
          rowAnnotationName: 'id',
          newRowAnnotationName: 'id',
          columnAnnotationName: 'id',
          newColumnAnnotationName: 'id'
        });
      }
      heatMapLoaded();
    });
  }
};

phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS = 6;

/**
 *
 * @param tool A tool instance
 * @param heatMap The calling heat map instance
 * @param callback Optional callback to invoke when tool is done
 */
phantasus.HeatMap.showTool = function (tool, heatMap, callback) {
  if (tool.gui) {
    var gui = tool.gui(heatMap.getProject());
    var formBuilder = new phantasus.FormBuilder();
    _.each(gui, function (item) {
      formBuilder.append(item);
    });
    var tabId = heatMap.getTabManager().getActiveTabId();
    if (tool.init) {
      tool.init(heatMap.getProject(), formBuilder, {
        heatMap: heatMap
      });
    }
    heatMap.trigger('beforeToolShown', {
      tool: tool,
      formBuilder: formBuilder
    });
    var okCallback = function () {
      var $dialogContent = $('<div><span>' + tool.toString() + '...</span><button class="btn' +
        ' btn-xs btn-default" style="margin-left:6px;display: none;">Cancel</button></div>');
      var value = null;

      var $dialog = phantasus.FormBuilder.showInDraggableDiv({
        $content: $dialogContent,
        appendTo: heatMap.getContentEl(),
        width: 'auto'
      });
      var input = {};
      _.each(gui, function (item) {
        input[item.name] = formBuilder.getValue(item.name);
      });
      // give ui a chance to update

      setTimeout(function () {
        value = tool.execute({
          heatMap: heatMap,
          project: heatMap.getProject(),
          input: input
        });
        if (value instanceof Worker) {
          $dialogContent.find('button').css('display', '').on('click', function () {
            value.terminate();
          });
          value.onerror = function (e) {
            value.terminate();
            phantasus.FormBuilder.showInModal({
              title: 'Error',
              html: e,
              close: 'Close',
              focus: heatMap.getFocusEl(),
              appendTo: heatMap.getContentEl()
            });
            if (e.stack) {
              console.log(e.stack);
            }
          };
          var terminate = _.bind(value.terminate, value);
          value.terminate = function () {
            terminate();
            $dialog.remove();
            if (callback) {
              callback(input);
            }
          };
        } else {
          if (value != null && typeof value.done === 'function') { // promise
            value.always(function () {
              if (callback) {
                callback(input);
              }
              $dialog.remove();
            });
          } else {
            if (callback) {
              callback(input);
            }
            $dialog.remove();
          }

        }
      }, 20);
      setTimeout(function () {
        // in case an exception was thrown
        if (!(value instanceof Worker)) {
          $dialog.remove();
        }
      }, 5000);

    };
    var $formDiv;
    tool.ok = function () {
      $formDiv.modal('hide');
      okCallback();
    };
    var guiOptions = $.extend({}, {
      ok: true
    }, gui.options);
    $formDiv = phantasus.FormBuilder.showOkCancel({
      title: tool.toString(),
      apply: tool.apply,
      ok: guiOptions.ok,
      cancel: guiOptions.cancel,
      size: guiOptions.size,
      draggable: true,
      content: formBuilder.$form,
      appendTo: heatMap.getContentEl(),
      align: 'right',
      okCallback: okCallback,
      focus: heatMap.getFocusEl()
    });
  }
  else { // run headless
    tool.execute({
      heatMap: heatMap,
      project: heatMap.getProject(),
      input: {}
    });
    if (callback) {
      callback({});
    }
  }
};

phantasus.HeatMap.getSpaces = function (groupByKeys, length, gapSize) {
  var previousArray = [];
  var nkeys = groupByKeys.length;
  for (var keyIndex = 0; keyIndex < nkeys; keyIndex++) {
    var key = groupByKeys[keyIndex];
    previousArray.push(key.getValue(0));
  }
  var spaces = [];
  var sum = 0;
  spaces.push(sum);
  for (var i = 1; i < length; i++) {
    var isEqual = true;
    for (var keyIndex = 0; keyIndex < nkeys; keyIndex++) {
      var key = groupByKeys[keyIndex];
      var comparator = key.getComparator();
      var val = key.getValue(i);
      var c = comparator(val, previousArray[keyIndex]);
      if (c !== 0) { // not equal, add space
        isEqual = false;
        for (var keyIndex2 = 0; keyIndex2 < nkeys; keyIndex2++) {
          previousArray[keyIndex2] = groupByKeys[keyIndex2].getValue(i);
        }
        break;
      }
    }
    if (!isEqual) {
      sum += gapSize;
    }
    spaces.push(sum);
  }
  return spaces;
};
phantasus.HeatMap.createGroupBySpaces = function (dataset, groupByKeys, gapSize, isColumns) {
  if (groupByKeys.length > 0) {
    var nkeys = groupByKeys.length;
    for (var keyIndex = 0; keyIndex < nkeys; keyIndex++) {
      groupByKeys[keyIndex].init(groupByKeys[keyIndex].isColumns() ? new phantasus.TransposedDatasetView(dataset) : dataset);
    }
    return phantasus.HeatMap.getSpaces(groupByKeys, isColumns ? dataset.getColumnCount() : dataset.getRowCount(),
      gapSize);
  }
};
phantasus.HeatMap.isDendrogramVisible = function (project, isColumns) {
  var sortKeys = isColumns ? project.getColumnSortKeys() : project.getRowSortKeys();
  if (sortKeys.length === 0) {
    return true;
  }
  // var filter = isColumns ? this.project.getColumnFilter()
  //   : this.project.getRowFilter();
  // // TODO compare filters
  var size = isColumns ? project.getSortedFilteredDataset().getColumnCount()
    : project.getSortedFilteredDataset().getRowCount();
  for (var i = 0; i < sortKeys.length; i++) {
    if (!sortKeys[i].isPreservesDendrogram() || sortKeys[i].nvisible !== size) {
      return false;
    }
  }
  return true;
};

phantasus.HeatMap.prototype = {
  updatingScroll: false,
  getWhitespaceEl: function () {
    return this.$whitespace;
  },
  getActionManager: function () {
    return this.actionManager;
  },
  autoDisplay: function (options) {
    if (options.filename == null) {
      options.filename = '';
    }
    var colorScheme;
    if (options.extension === 'segtab' || options.extension === 'seg') {
      colorScheme = {
        scalingMode: 'fixed',
        values: phantasus.HeatMapColorScheme.Predefined.CN().values.map(function (value) {
          return Math.pow(2, 1 + value);
        }),
        colors: phantasus.HeatMapColorScheme.Predefined.CN().colors
      };
    } else if (options.extension === 'maf') {
      colorScheme = phantasus.HeatMapColorScheme.Predefined.MAF();
      var rowMutProfile = this.project.getFullDataset().getRowMetadata().getByName('mutation_summary');
      var fieldNames = rowMutProfile.getProperties().get(phantasus.VectorKeys.FIELDS);
      var useMafColorMap = true;
      if (fieldNames.length !== phantasus.MafFileReader.FIELD_NAMES.length) {
        useMafColorMap = false;
      } else {
        for (var i = 0; i < fieldNames.length; i++) {
          if (fieldNames[i] !== phantasus.MafFileReader.FIELD_NAMES[i]) {
            useMafColorMap = false;
            break;
          }
        }
      }
      if (!useMafColorMap) {
        colorScheme = {
          scalingMode: 'fixed',
          stepped: true,
          values: [0],
          colors: ['rgb(255,255,255)']
        };
        for (var i = 0; i < fieldNames.length; i++) {
          colorScheme.values.push(i + 1);
          colorScheme.colors.push(phantasus.VectorColorModel.TWENTY_COLORS[i % phantasus.VectorColorModel.TWENTY_COLORS.length]);
          colorScheme.names.push(fieldNames[i]);
        }
      }
      var columnMutationSummaryVectors = [];
      var columnMutationSummaryNames = ['mutation_summary', 'mutation_summary_selection'];
      for (var i = 0; i < columnMutationSummaryNames.length; i++) {
        var name = columnMutationSummaryNames[i];
        if (this.project.getFullDataset().getColumnMetadata().getByName(name)) {
          columnMutationSummaryVectors.push(this.project.getFullDataset().getColumnMetadata().getByName(name));
          track = this.getTrack(name, true);
          if (track) {
            track.settingFromConfig('stacked_bar');
            if (name === 'mutation_summary_selection') {
              track.settings.autoscaleAlways = true;
            }
          }
        }
      }

      var track = this.getTrack('mutation_summary', false);
      if (track) {
        track.settingFromConfig('stacked_bar');
      }

      for (var i = 1; i < colorScheme.colors.length; i++) {
        if (rowMutProfile) {
          this.getProject().getRowColorModel().setMappedValue(
            rowMutProfile, i - 1, colorScheme.colors[i]);
        }
        for (var j = 0; j < columnMutationSummaryVectors.length; j++) {
          this.getProject().getColumnColorModel().setMappedValue(
            columnMutationSummaryVectors[j], i - 1, colorScheme.colors[i]);
        }

      }
    } else if (options.extension === 'gmt') {
      colorScheme = phantasus.HeatMapColorScheme.Predefined.BINARY();
    } else if (options.filename === 'all_lesions.conf_99'
      || options.filename === 'all_data_by_genes.txt' || options.filename.toLowerCase().indexOf('gistic') !== -1) {
      colorScheme = {
        scalingMode: 'fixed',
        values: [-0.5, 0, 0.5],
        colors: ['blue', 'white', 'red']
      };
    } else if (options.filename.toLowerCase().indexOf('copynumber') !== -1 ||
      options.filename.toLowerCase().indexOf('copy number') !== -1) {
      colorScheme = {
        scalingMode: 'fixed',
        values: [-1.5, 0, 1.5],
        colors: ['blue', 'white', 'red']
      };
    } else if (options.filename.toLowerCase().indexOf('achilles') !== -1) {
      colorScheme = {
        scalingMode: 'fixed',
        values: [-3, -1, 1, 3],
        colors: ['blue', 'white', 'white', 'red']
      };
    }

    if (colorScheme && options.filename && this.heatmap.getColorScheme()) {
      this.heatmap.getColorScheme().setCurrentValue(options.filename);
      this.heatmap.getColorScheme().setColorSupplierForCurrentValue(
        phantasus.AbstractColorSupplier.fromJSON(colorScheme));
    }
    return colorScheme;
  },
  /**
   *
   * @param sortOrder
   * @param isColumns
   *            Whether sorting based on column selection
   * @param append
   *            Whether to add to existing sort order
   */
  sortBasedOnSelection: function (sortOrder, isColumns, append) {
    // if isColumns, sort rows
    var project = this.project;
    var selectionModel = isColumns ? project.getColumnSelectionModel()
      : project.getRowSelectionModel();
    var modelIndices = selectionModel.toModelIndices();
    if (modelIndices.length === 0) {
      return;
    }

    var priorSortKeyIndex = -1;
    if (sortOrder == null) {
      // toggle sort order?
      var existingSortKeys = isColumns ? project.getRowSortKeys()
        : project.getColumnSortKeys();
      for (var i = 0, length = existingSortKeys.length; i < length; i++) {
        var key = existingSortKeys[i];
        if (key instanceof phantasus.SortByValuesKey
          && phantasus.Util.arrayEquals(key.modelIndices,
            modelIndices)) {
          priorSortKeyIndex = i;
          if (key.getSortOrder() === phantasus.SortKey.SortOrder.UNSORTED) {
            sortOrder = phantasus.SortKey.SortOrder.DESCENDING; // 1st
            // click
          } else if (key.getSortOrder() === phantasus.SortKey.SortOrder.DESCENDING) {
            sortOrder = phantasus.SortKey.SortOrder.ASCENDING; // 2nd
            // click
          } else if (key.getSortOrder() === phantasus.SortKey.SortOrder.ASCENDING) {
            sortOrder = phantasus.SortKey.SortOrder.TOP_N; // 3rd
            // click
          } else if (key.getSortOrder() === phantasus.SortKey.SortOrder.TOP_N) {
            sortOrder = phantasus.SortKey.SortOrder.UNSORTED; // 4th
            // click
          }
          break;
        }
      }

    }

    if (sortOrder == null) {
      sortOrder = phantasus.SortKey.SortOrder.DESCENDING;
    }

    var sortKeys;
    if (append) {
      sortKeys = !isColumns ? project.getColumnSortKeys() : project.getRowSortKeys();

      if (priorSortKeyIndex !== -1) {
        if (sortOrder === phantasus.SortKey.SortOrder.UNSORTED) {
          // remove existing sort key
          sortKeys.splice(priorSortKeyIndex, 1);
        } else {
          sortKeys[priorSortKeyIndex].setSortOrder(sortOrder);
        }

      } else {
        if (sortOrder !== phantasus.SortKey.SortOrder.UNSORTED) {
          sortKeys.push(new phantasus.SortByValuesKey(modelIndices,
            sortOrder, !isColumns));
        }
        // add new sort key
      }

      sortKeys = phantasus.SortKey.keepExistingSortKeys(sortKeys,
        !isColumns ? project.getColumnSortKeys() : project.getRowSortKeys());

    } else {
      var newSortKeys = sortOrder === phantasus.SortKey.SortOrder.UNSORTED ? []
        : [
          new phantasus.SortByValuesKey(modelIndices, sortOrder,
            !isColumns)];
      sortKeys = phantasus.SortKey.keepExistingSortKeys(newSortKeys,
        !isColumns ? project.getColumnSortKeys() : project.getRowSortKeys());
    }

    if (!isColumns) { // sort columns by selected rows
      project.setColumnSortKeys(sortKeys, true);
      this.scrollLeft(0);
    } else { // sort rows by selected column
      project.setRowSortKeys(sortKeys, true);
      this.scrollTop(0);
    }
    phantasus.Util.trackEvent({
      eventCategory: 'Tool',
      eventAction: isColumns ? 'sortRowsBasedOnSelection' : 'sortColumnsBasedOnSelection'
    });

  },
  getToolbarElement: function () {
    return this.toolbar.$el;
  },
  getToolbar: function () {
    return this.toolbar;
  },
  setName: function (name) {
    this.options.name = name;
  },
  getName: function () {
    return this.options.name;
  },
  showOptions: function () {
    new phantasus.HeatMapOptions(this);
  },
  getProject: function () {
    return this.project;
  },
  getDendrogram: function (isColumns) {
    return isColumns ? this.columnDendrogram : this.rowDendrogram;
  },
  toJSON: function (options) {
    var _this = this;
    var json = {};
    // color scheme
    json.colorScheme = this.heatmap.getColorScheme().toJSON();

    json.name = this.options.name;

    json.showRowNumber = this.isShowRowNumber();

    // annotation shapes
    json.rowShapeModel = this.getProject().getRowShapeModel().toJSON(this.getVisibleTracks(false));
    json.columnShapeModel = this.getProject().getColumnShapeModel().toJSON(this.getVisibleTracks(true));

    // annotation font
    json.rowFontModel = this.getProject().getRowFontModel().toJSON(this.getVisibleTracks(false));
    json.columnFontModel = this.getProject().getColumnFontModel().toJSON(this.getVisibleTracks(true));

    // annotation colors
    json.rowColorModel = this.getProject().getRowColorModel().toJSON(this.getVisibleTracks(false));
    json.columnColorModel = this.getProject().getColumnColorModel().toJSON(this.getVisibleTracks(true));
    // annotation display
    json.rows = this.getVisibleTracks(false).map(function (track) {
      var size = phantasus.CanvasUtil.getPreferredSize(_this.getTrackHeaderByIndex(_this.getTrackIndex(track.getName(), false), false));
      var obj = track.settings;
      obj.field = track.getName();
      obj.size = {
        width: size.widthSet ? size.width : undefined
      };
      return obj;
    });
    json.columns = this.getVisibleTracks(true).map(function (track) {
      var size = phantasus.CanvasUtil.getPreferredSize(_this.getTrackHeaderByIndex(_this.getTrackIndex(track.getName(), true), true));
      var obj = track.settings;
      obj.field = track.getName();
      obj.size = {
        width: size.widthSet ? size.width : undefined,
        height: size.heightSet ? size.height : undefined
      };
      return obj;
    });

    // sort
    json.rowSortBy = phantasus.SortKey.toJSON(this.getProject().getRowSortKeys());
    json.columnSortBy = phantasus.SortKey.toJSON(this.getProject().getColumnSortKeys());

    // group
    json.rowGroupBy = phantasus.SortKey.toJSON(this.getProject().getGroupRows());
    json.columnGroupBy = phantasus.SortKey.toJSON(this.getProject().getGroupColumns());

    // filter
    json.rowFilter = phantasus.CombinedFilter.toJSON(this.getProject().getRowFilter());
    json.columnFilter = phantasus.CombinedFilter.toJSON(this.getProject().getColumnFilter());

    // element size, symmetric
    json.symmetric = this.options.symmetric;
    json.rowSize = this.heatmap.getRowPositions().getSize();
    json.columnSize = this.heatmap.getColumnPositions().getSize();
    json.rowGapSize = this.heatmap.rowGapSize;
    json.columnGapSize = this.heatmap.columnGapSize;
    json.drawGrid = this.heatmap.isDrawGrid();
    json.gridColor = this.heatmap.getGridColor();
    json.gridThickness = this.heatmap.getGridThickness();
    json.drawValues = this.heatmap.isDrawValues();

    // selection
    json.rowSelection = this.getProject().getRowSelectionModel().toModelIndices();
    json.columnSelection = this.getProject().getColumnSelectionModel().toModelIndices();

    // search terms
    json.rowSearchTerm = this.toolbar.getSearchField(phantasus.HeatMapToolBar.ROW_SEARCH_FIELD).val();
    json.columnSearchTerm = this.toolbar.getSearchField(phantasus.HeatMapToolBar.COLUMN_SEARCH_FIELD).val();

    //  dendrogram
    if (this.rowDendrogram != null) {
      var out = [];
      phantasus.DendrogramUtil.writeNewick(this.rowDendrogram.tree.rootNode, out, function (n) {
        return n.index;
      });
      json.rowDendrogram = out.join('');
      json.rowDendrogramField = null;
    }
    if (this.columnDendrogram != null) {
      var out = [];
      phantasus.DendrogramUtil.writeNewick(this.columnDendrogram.tree.rootNode, out, function (n) {
        return n.index;
      });
      json.columnDendrogram = out.join('');
      json.columnDendrogramField = null;
    }
    if (options.dataset) {
      json.dataset = phantasus.Dataset.toJSON(this.getProject().getFullDataset());
    }

    return json;
  },
  /**
   * @param tree
   *            An object with maxHeight, a rootNode, leafNodes, and
   *            nLeafNodes
   */
  setDendrogram: function (tree, isColumns, modelOrder) {
    var dendrogram = isColumns ? this.columnDendrogram : this.rowDendrogram;
    if (dendrogram) {
      dendrogram.dispose();
      dendrogram = null;
    }
    if (tree != null) {
      //  var modelIndexSet = new phantasus.Set();
      var size = isColumns ? this.project.getFullDataset().getColumnCount() : this.project.getFullDataset().getRowCount();
      if (isColumns) {
        dendrogram = new phantasus.ColumnDendrogram(this, tree,
          this.heatmap.getColumnPositions(), this.project);
        dendrogram.filter = this.project.getColumnFilter().shallowClone();
        this.columnDendrogram = dendrogram;
        var sortKey = new phantasus.SpecifiedModelSortOrder(modelOrder,
          modelOrder.length, 'dendrogram', true);
        sortKey.setPreservesDendrogram(true);
        sortKey.setLockOrder(2);
        sortKey.setUnlockable(false);
        this.project.setColumnSortKeys(
          [sortKey], true);
      } else {
        dendrogram = new phantasus.RowDendrogram(this, tree,
          this.heatmap.getRowPositions(), this.project);
        dendrogram.filter = this.project.getRowFilter().shallowClone();
        this.rowDendrogram = dendrogram;
        var sortKey = new phantasus.SpecifiedModelSortOrder(modelOrder,
          modelOrder.length, 'dendrogram', false);
        sortKey.setPreservesDendrogram(true);
        sortKey.setLockOrder(2);
        sortKey.setUnlockable(false);
        this.project.setRowSortKeys(
          [sortKey], true);
      }
      dendrogram.appendTo(this.$parent);
      dendrogram.$label.appendTo(this.$parent);
      dendrogram.$squishedLabel.appendTo(this.$parent);

    } else { // no more dendrogram
      var sortKeys = isColumns ? this.project.getColumnSortKeys()
        : this.project.getRowSortKeys();
      // remove dendrogram sort key
      for (var i = 0; i < sortKeys.length; i++) {
        if (sortKeys[i].isPreservesDendrogram()) {
          sortKeys.splice(i, 1);
          i--;
        }
      }
      if (isColumns) {
        this.heatmap.getColumnPositions().setSquishedIndices(null);
        delete this.columnDendrogram;
        this.project.setColumnSortKeys(sortKeys, true);
      } else {
        delete this.rowDendrogram;
        this.project.setRowSortKeys(sortKeys, true);
        this.heatmap.getRowPositions().setSquishedIndices(null);
      }

    }
    // FIXME update grouping
    this.trigger('dendrogramChanged', {
      isColumns: isColumns
    });
  },
  getTabManager: function () {
    return this.tabManager;
  },
  getSelectedElementsText: function () {
    var _this = this;
    var project = this.project;
    var selectedViewIndices = project.getElementSelectionModel().getViewIndices();
    if (selectedViewIndices.size() > 0) {
      var tipText = [];
      var dataset = project.getSortedFilteredDataset();
      var rowTracks = _this.rowTracks.filter(function (t) {
        return t.settings.inlineTooltip;
      });
      var columnTracks = _this.columnTracks.filter(function (t) {
        return t.settings.inlineTooltip;
      });
      selectedViewIndices.forEach(function (id) {
        var rowIndex = id.getArray()[0];
        var columnIndex = id.getArray()[1];
        tipText.push(phantasus.Util.nf(dataset.getValue(rowIndex,
          columnIndex)));
        rowTracks.forEach(function (track) {
          tipText.push('\t');
          tipText.push(phantasus.Util.toString(dataset.getRowMetadata().getByName(track.name).getValue(
            rowIndex)));
        });
        columnTracks.forEach(function (track) {
          tipText.push('\t');
          tipText.push(phantasus.Util.toString(dataset.getColumnMetadata().getByName(track.name).getValue(columnIndex)));
        });

        tipText.push('\n');

      });
      return tipText.join('');

    }

  },
  _init: function () {
    var _this = this;
    phantasus.MetadataUtil.renameFields(this.options.dataset, this.options);
    var dataset = this.options.dataset;
    var rowDendrogram = this.options.rowDendrogram;
    var columnDendrogram = this.options.columnDendrogram;
    _.each(this.whenLoaded, function (f) {
      f(_this.options.dataset);
    });
    if (this.options.datasetReady) {
      var updatedDataset = this.options.datasetReady(dataset);
      if (updatedDataset) {
        dataset = updatedDataset;
      }
    }

    this.project = new phantasus.Project(dataset);

    if (this.tabManager) {
      this.tabManager.setTabTitle(this.tabId, this.project.getFullDataset().getRowCount()
        + ' row'
        + phantasus.Util.s(this.project.getFullDataset().getRowCount())
        + ' x '
        + this.project.getFullDataset().getColumnCount()
        + ' column'
        + phantasus.Util.s(this.project.getFullDataset().getColumnCount()));
    }
    if (this.options.inheritFromParent && this.options.parent != null) {
      phantasus.HeatMap.copyFromParent(this.project, this.options);
    }

    // filter ui will be initialized automatically
    if (this.options.rowFilter) {
      phantasus.CombinedFilter.fromJSON(_this.project.getRowFilter(), this.options.rowFilter);
      _this.project.setRowFilter(_this.project.getRowFilter(), true);
    }
    if (this.options.columnFilter) {
      phantasus.CombinedFilter.fromJSON(_this.project.getColumnFilter(), this.options.columnFilter);
      _this.project.setColumnFilter(_this.project.getColumnFilter(), true);
    }
    this.whenLoaded = null;
    this.$parent = $('<div></div>').css('position', 'relative');

    this.$parent.appendTo(this.$content);
    if (!phantasus.Util.isHeadless()) {
      this.toolbar = new phantasus.HeatMapToolBar(this);
    }

    // scroll bars at the bottom of the heatmap, and right of the heatmap
    // TODO along bottom of row metadata, and along left of column metadata
    // the viewport is the size of the visible region, the view is the full
    // size of the heat map
    this.vscroll = new phantasus.ScrollBar(true);
    this.vscroll.appendTo(this.$parent);
    this.vscroll.on('scroll', function () {
      if (_this.updatingScroll) {
        return;
      }
      _this.paintAll({
        paintRows: true,
        paintColumns: false,
        invalidateRows: true,
        invalidateColumns: false
      });
    });

    // for resizing column dendrogram
    this.beforeColumnTrackDivider = new phantasus.Divider(false);
    this.beforeColumnTrackDivider.appendTo(this.$parent);
    var dragStartHeight = 0;
    this.beforeColumnTrackDivider.on('resizeStart', function (e) {
      dragStartHeight = _this.columnDendrogram.getUnscaledHeight();
    }).on('resize', function (e) {
      // grow or shrink the column dendrogram
      var newHeight = Math.max(8, dragStartHeight + e.delta);
      _this.columnDendrogram.setPrefHeight(newHeight);
      _this.revalidate();
    }).on('resizeEnd', function () {
      dragStartHeight = 0;
    });

    // for resizing row dendrogram
    this.afterRowDendrogramDivider = new phantasus.Divider(true);
    this.afterRowDendrogramDivider.appendTo(this.$parent);
    var rowDendrogramStartWidth = 0;
    this.afterRowDendrogramDivider.on('resizeStart', function (e) {
      rowDendrogramStartWidth = _this.rowDendrogram.getUnscaledWidth();
    }).on('resize', function (e) {
      // grow or shrink the column dendrogram
      var newWidth = Math.max(8, rowDendrogramStartWidth + e.delta);
      _this.rowDendrogram.setPrefWidth(newWidth);
      _this.revalidate();
    }).on('resizeEnd', function () {
      rowDendrogramStartWidth = 0;
    });

    this.afterVerticalScrollBarDivider = new phantasus.Divider(true);
    this.afterVerticalScrollBarDivider.appendTo(this.$parent);
    var resizeStartHeatMapWidth = 0;
    this.afterVerticalScrollBarDivider.on('resizeStart', function (e) {
      resizeStartHeatMapWidth = _this.heatmap.getUnscaledWidth();
    }).on('resize', function (e) {
      // grow or shrink the heat map
      _this.heatmap.prefWidth = resizeStartHeatMapWidth + e.delta;
      _this.revalidate();
    });
    // horizontal scroll
    this.hscroll = new phantasus.ScrollBar(false);
    this.hscroll.appendTo(this.$parent);
    this.hscroll.on('scroll', function () {
      if (_this.updatingScroll) {
        return;
      }
      _this.paintAll({
        paintRows: false,
        paintColumns: true,
        invalidateRows: false,
        invalidateColumns: true
      });
    });
    this.$whitespace = $('<div style="position: absolute;"></div>');
    this.$whitespace.appendTo(this.$parent);
    var heatmap = new phantasus.HeatMapElementCanvas(this.project);
    if (this.options.drawCallback) {
      heatmap.setDrawCallback(this.options.drawCallback);
    }

    $(heatmap.canvas).on(
      'contextmenu',
      function (e) {
        var items = [];
        phantasus.Popup.showPopup(
          [

            {
              name: 'Copy Image',
              class: 'copy'
            },
            {
              name: 'Save Image (' + phantasus.Util.COMMAND_KEY + 'S)'
            },
            //{
            // name: 'Copy Selection',
            //  disabled: _this.project
            //  .getElementSelectionModel()
            //  .count() === 0,
            //  class: 'copy'
            //},
            {
              separator: true
            },
            {
              name: 'Show Inline Tooltip',
              checked: _this.options.inlineTooltip
            }],
          {
            x: e.pageX,
            y: e.pageY
          },
          e.target,
          function (event, item) {
            if (item === 'Show Inline Tooltip') {
              _this.options.inlineTooltip = !_this.options.inlineTooltip;
            } else if (item === ('Save Image (' + phantasus.Util.COMMAND_KEY + 'S)')) {
              _this.getActionManager().execute('Save Image');
            } else if (item === 'Copy Selection') {
              var text = _this.getSelectedElementsText();
              if (text !== '') {
                event.clipboardData.setData(
                  'text/plain',
                  text);
              }
            } else if (item === 'Copy Image') {
              _this.getActionManager().execute('Copy Image', {event: event});
            } else {
              //console.log(item + ' unknown.');
            }
          });

          e.preventDefault();
          e.stopPropagation();
          e.stopImmediatePropagation();
        });
    heatmap.appendTo(this.$parent);
    this.heatmap = heatmap;
    var rowDendrogramSortKey = null;
    if (rowDendrogram != null) {
      var tree = rowDendrogram;
      if (tree.leafNodes.length !== this.project.getSortedFilteredDataset().getRowCount()) {
        throw '# leaf nodes in row dendrogram ' + tree.leafNodes.length
        + ' != ' + this.project.getSortedFilteredDataset().getRowCount();
      }
      var rowIndices = null;
      // when saving a session the dataset is reordered to reflect the clustering
      if (this.options.rowDendrogramField != null) {
        var vector = dataset.getRowMetadata().getByName(
          this.options.rowDendrogramField);
        rowIndices = [];
        var map = new phantasus.Map();
        var re = /[,:]/g;
        for (var j = 0, size = vector.size(); j < size; j++) {
          var key = vector.getValue(j);
          map.set(key.replace(re, ''), j);
        }
        // need to replace special characters to match ids in newick
        // file

        for (var i = 0, length = tree.leafNodes.length; i < length; i++) {
          var index = map.get(tree.leafNodes[i].name);
          if (index === undefined) {
            throw 'Unable to find row dendrogram id '
            + tree.leafNodes[i].name
            + ' in row annotations';
          }
          rowIndices.push(index);
        }
      } else {
        // see if leaf node ids are indices
        // for (var i = 0, length = tree.leafNodes.length; i < length; i++) {
        //   var newickId = tree.leafNodes[i].name;
        //   newickId = parseInt(newickId);
        //   if (!isNaN(newickId)) {
        //     rowIndices.push(newickId);
        //   } else {
        //     break;
        //   }
        // }
        // if (rowIndices.length !== tree.leafNodes.length) {
        //   rowIndices = [];
        //   for (var i = 0, length = tree.leafNodes.length; i < length; i++) {
        //     rowIndices.push(i);
        //   }
        // }
      }
      this.rowDendrogram = new phantasus.RowDendrogram(this, tree, heatmap.getRowPositions(), this.project, true);
      this.rowDendrogram.appendTo(this.$parent);
      this.rowDendrogram.$label.appendTo(this.$parent);
      this.rowDendrogram.$squishedLabel.appendTo(this.$parent);
      if (rowIndices != null) {
        rowDendrogramSortKey = new phantasus.SpecifiedModelSortOrder(
          rowIndices, rowIndices.length, 'dendrogram');
        rowDendrogramSortKey.setLockOrder(2);
        rowDendrogramSortKey.setUnlockable(false);
        rowDendrogramSortKey.setPreservesDendrogram(true);
      }
    }
    var columnDendrogramSortKey = null;
    if (columnDendrogram != null) {
      var tree = columnDendrogram;

      if (tree.leafNodes.length !== this.project.getSortedFilteredDataset().getColumnCount()) {
        throw '# leaf nodes ' + tree.leafNodes.length + ' != '
        + this.project.getSortedFilteredDataset().getColumnCount();
      }
      var columnIndices = null;
      if (this.options.columnDendrogramField != null) {
        columnIndices = [];
        var vector = dataset.getColumnMetadata().getByName(
          this.options.columnDendrogramField);
        var map = new phantasus.Map();
        var re = /[,:]/g;
        for (var j = 0, size = vector.size(); j < size; j++) {
          var key = vector.getValue(j);
          map.set(key.replace(re, ''), j);
        }

        for (var i = 0, length = tree.leafNodes.length; i < length; i++) {
          var index = map.get(tree.leafNodes[i].name);
          if (index === undefined) {
            throw 'Unable to find column dendrogram id '
            + tree.leafNodes[i].name
            + ' in column annotations';
          }
          columnIndices.push(index);
        }
      } else {
        // for (var i = 0, length = tree.leafNodes.length; i < length; i++) {
        //   var newickId = tree.leafNodes[i].name;
        //   newickId = parseInt(newickId);
        //   if (!isNaN(newickId)) {
        //     columnIndices.push(newickId);
        //   } else {
        //     break;
        //   }
        // }
        // if (columnIndices.length !== tree.leafNodes.length) {
        //   columnIndices = [];
        //   for (var i = 0, length = tree.leafNodes.length; i < length; i++) {
        //     columnIndices.push(i);
        //   }
        // }
      }
      this.columnDendrogram = new phantasus.ColumnDendrogram(this, tree,
        heatmap.getColumnPositions(), this.project, true);
      this.columnDendrogram.appendTo(this.$parent);
      this.columnDendrogram.$label.appendTo(this.$parent);
      this.columnDendrogram.$squishedLabel.appendTo(this.$parent);
      if (columnIndices != null) {
        columnDendrogramSortKey = new phantasus.SpecifiedModelSortOrder(
          columnIndices, columnIndices.length, 'dendrogram');
        columnDendrogramSortKey.setLockOrder(2);
        columnDendrogramSortKey.setUnlockable(false);
        columnDendrogramSortKey.setPreservesDendrogram(true);
      }
    }

    if (this.options.drawGrid != null) {
      this.heatmap.setDrawGrid(this.options.drawGrid);
    }

    if (this.options.gridColor != null) {
      this.heatmap.setGridColor(this.options.gridColor);
    }
    if (this.options.gridThickness != null) {
      this.heatmap.setGridThickness(this.options.gridThickness);
    }
    if (this.options.drawValues != null) {
      this.heatmap.setDrawValues(this.options.drawValues);
    }

    if (rowDendrogramSortKey != null) {
      this.project.setRowSortKeys([rowDendrogramSortKey]);
    }
    if (columnDendrogramSortKey != null) {
      this.project.setColumnSortKeys([columnDendrogramSortKey]);
    }
    if (this.options.rowSortBy && this.options.rowSortBy.length > 0) {
      this.project.setRowSortKeys(phantasus.SortKey.fromJSON(this.project, this.options.rowSortBy), false);
    }
    if (this.options.columnSortBy && this.options.columnSortBy.length > 0) {
      this.project.setColumnSortKeys(phantasus.SortKey.fromJSON(this.project, this.options.columnSortBy), false);
    }
    if (this.options.rowGroupBy != null && this.options.rowGroupBy.length > 0) {
      var keys = phantasus.SortKey.fromJSON(this.project, this.options.rowGroupBy);
      for (var i = 0; i < keys.length; i++) {
        this.project.groupRows.push(keys[i]);
      }
    }
    if (this.options.columnGroupBy != null && this.options.columnGroupBy.length > 0) {
      var keys = phantasus.SortKey.fromJSON(this.project, this.options.columnGroupBy);
      for (var i = 0; i < keys.length; i++) {
        this.project.groupColumns.push(keys[i]);
      }
    }
    if (this.options.rowSelection != null && this.options.rowSelection.length > 0) {
      var indices = new phantasus.Set();
      for (var i = 0, length = this.options.rowSelection.length; i < length; i++) {
        indices.add(this.project.convertModelRowIndexToView(this.options.rowSelection[i]));
      }
      this.project.getRowSelectionModel().setViewIndices(indices, false);
    }
    if (this.options.columnSelection != null && this.options.columnSelection.length > 0) {
      var indices = new phantasus.Set();
      for (var i = 0, length = this.options.columnSelection.length; i < length; i++) {
        indices.add(this.project.convertModelColumnIndexToView(this.options.columnSelection[i]));
      }
      this.project.getColumnSelectionModel().setViewIndices(indices, false);
    }
    // if (this.options.rowSearchTerm != null && this.options.rowSearchTerm !== '') {
    //   this.toolbar.getSearchField(phantasus.HeatMapToolBar.ROW_SEARCH_FIELD).val(this.options.rowSearchTerm);
    // }
    // if (this.options.columnSearchTerm != null && this.options.columnSearchTerm !== '') {
    //   this.toolbar.getSearchField(phantasus.HeatMapToolBar.COLUMN_SEARCH_FIELD).val(this.options.columnSearchTerm);
    // }

    this.vSortByValuesIndicator = new phantasus.SortByValuesIndicator(
      this.project, true, heatmap.getRowPositions());
    this.vSortByValuesIndicator.appendTo(this.$parent);
    this.hSortByValuesIndicator = new phantasus.SortByValuesIndicator(
      this.project, false, heatmap.getColumnPositions());
    this.hSortByValuesIndicator.appendTo(this.$parent);
    this.verticalSearchBar = new phantasus.ScentedSearch(this.project.getRowSelectionModel(), heatmap.getRowPositions(), true,
      this.vscroll, this);
    this.horizontalSearchBar = new phantasus.ScentedSearch(this.project.getColumnSelectionModel(), heatmap.getColumnPositions(),
      false, this.hscroll, this);
    this.rowTracks = [];
    this.rowTrackHeaders = [];
    this.columnTracks = [];
    this.columnTrackHeaders = [];
    if (this.options.rowSize != null && this.options.rowSize !== 'fit') {
      this.heatmap.getRowPositions().setSize(this.options.rowSize);
    }
    if (this.options.columnSize != null && this.options.columnSize !== 'fit') {
      this.heatmap.getColumnPositions().setSize(
        this.options.columnSize);
    }
    var setInitialDisplay = function (isColumns, options) {
      var nameToOption = new phantasus.Map();
      // at
      // least
      // one
      // display option
      // supplied

      var displaySpecified = (_this.options.parent != null && _this.options.inheritFromParent);
      if (options != null && options.length > 0) {
        displaySpecified = true;
        for (var i = 0; i < options.length; i++) {
          nameToOption.set(options[i].renameTo != null ? options[i].renameTo
            : options[i].field, options[i]);
        }
      }

      var displayMetadata = isColumns ? dataset.getColumnMetadata()
        : dataset.getRowMetadata();
      // see if default fields found
      if (!displaySpecified) {
        var defaultFieldsToShow = new phantasus.Set();
        ['pert_iname', 'moa', 'target', 'cell_id', 'pert_type'].forEach(function (field) {
          defaultFieldsToShow.add(field);
        });
        for (var i = 0, metadataCount = displayMetadata.getMetadataCount(); i < metadataCount; i++) {
          var v = displayMetadata.get(i);
          if (defaultFieldsToShow.has(v.getName()) && !nameToOption.has(v.getName())) {
            nameToOption.set(v.getName(), {
              display: ['text']
            });
            displaySpecified = true;
          }
        }
      }
      var isFirst = true;
      // console.log("heat_map ::", displayMetadata, displaySpecified);
      for (var i = 0, metadataCount = displayMetadata.getMetadataCount(); i < metadataCount; i++) {
        var v = displayMetadata.get(i);
        var name = v.getName();
        var option = nameToOption.get(name);
        if (displaySpecified && option == null) {
          continue;
        }
        if (phantasus.MetadataUtil.DEFAULT_HIDDEN_FIELDS.has(name)
          && option == null) {
          continue;
        }
        var count = isColumns ? dataset.getColumnCount() : dataset.getRowCount();
        if (option == null && !displaySpecified && count > 1
          && !phantasus.VectorUtil.containsMoreThanOneValue(v)) {
          continue;
        }
        if (option == null) {
          option = {};
        }
        if (option.title) {
          v.getProperties().set(phantasus.VectorKeys.TITLE,
            option.title);
        }

        if (option.display == null) {
          if (name === 'pert_iname' || name === 'id' || isFirst) {
            option.inlineTooltip = true;
            option.display = ['text'];
          } else {
            option.display = isColumns ? 'color,highlight' : 'text';
          }
        }
        isFirst = false;
        var track = _this.addTrack(name, isColumns, option);

        if (option.size) {
          if (!isColumns && option.size.width != null) {
            var header = _this.getTrackHeaderByIndex(_this.getTrackIndex(name, isColumns), isColumns);
            track.setPrefWidth(option.size.width); // can only set width
            header.setPrefWidth(option.size.width);
          } else if (isColumns && (option.size.width != null || option.size.height != null)) {
            var header = _this.getTrackHeaderByIndex(_this.getTrackIndex(name, isColumns), isColumns);
            if (option.size.height) {
              track.setPrefHeight(option.size.height);
              header.setPrefHeight(option.size.height);
            }
            if (option.size.width) {
              // TODO set width for all tracks since they all have same width
              track.setPrefWidth(option.size.width);
              header.setPrefWidth(option.size.width);
            }
          }
        }
        if (option.header && option.header.font) {
          var header = _this.getTrackHeaderByIndex(_this.getTrackIndex(name, isColumns), isColumns);
          header.font = option.header.font;
        }
        if (option.formatter) {
          v.getProperties().set(phantasus.VectorKeys.FORMATTER, phantasus.Util.createNumberFormat(option.formatter));
        }
        if (option.formatter) {
          v.getProperties().set(phantasus.VectorKeys.FORMATTER, phantasus.Util.createNumberFormat(option.formatter));
        }
        if (track.isRenderAs(phantasus.VectorTrack.RENDER.COLOR)
          && option.color) {
          var m = isColumns ? _this.project.getColumnColorModel()
            : _this.project.getRowColorModel();
          if (track.getFullVector().getProperties.get(phantasus.VectorKeys.DISCRETE)) {
            _.each(options.color, function (p) {
              m.setMappedValue(v, p.value, p.color);
            });
          } else {
            var cs = m.createContinuousColorMap(v);
            var min = Number.MAX_VALUE;
            var max = -Number.MAX_VALUE;
            _.each(options.color, function (p) {
              min = Math.min(min, p.value);
              max = Math.max(max, p.value);
            });

            cs.setMin(min);
            cs.setMax(max);
            var valueToFraction = d3.scale.linear().domain(
              [cs.getMin(), cs.getMax()]).range(
              [0, 1]).clamp(true);
            var fractions = [];
            var colors = [];
            _.each(options.color, function (p) {
              fractions.push(valueToFraction(p.value));
              colors.push(p.color);
            });

            cs.setFractions({
              fractions: fractions,
              colors: colors
            });
          }

          if (track.isRenderAs(phantasus.VectorTrack.RENDER.SHAPE)
            && option.shape) {
            var m = isColumns ? _this.project.getColumnShapeModel()
              : _this.project.getRowShapeModel();
            _.each(options.shape, function (p) {
              m.setMappedValue(v, p.value, p.shape);
            });

          }
        }
      }
    };
    setInitialDisplay(false, this.options.rows);
    setInitialDisplay(true, this.options.columns);

    function reorderTracks(array, isColumns) {
      if (array == null || array.length <= 1) {
        return;
      }
      var nameOrderPairs = [];
      var found = false;
      array.forEach(function (item, index) {

        var name = item.renameTo || item.field;
        var order = index;
        if (item.order != null) {
          order = item.order;
          found = true;
        }
        nameOrderPairs.push({
          name: name,
          order: order
        });
      });
      if (!found) {
        array.forEach(function (item, index) {
          var name = item.renameTo || item.field;
          nameOrderPairs.push({
            name: name,
            order: index
          });
        });
      }

      nameOrderPairs.sort(function (a, b) {
        return (a.order === b.order ? 0 : (a.order < b.order ? -1
          : 1));
      });
      for (var i = 0, counter = 0; i < nameOrderPairs.length; i++) {
        var index = _this.getTrackIndex(nameOrderPairs[i].name,
          isColumns);
        if (index !== -1) {
          _this.moveTrack(index, counter, isColumns);
          counter++;
        }
      }

    }

    reorderTracks(this.options.rows, false);
    reorderTracks(this.options.columns, true);

    if (this.options.showRowNumber) {
      this.setShowRowNumber(true);
    }
    var colorSchemeSpecified = this.options.colorScheme != null;
    if (this.options.colorScheme == null) {
      var ext = '';
      if (this.options.dataSource) {
        ext = phantasus.Util.getExtension(phantasus.Util.getFileName(this.options.dataSource));
      }

      var colorScheme = this.autoDisplay({
        filename: phantasus.Util.getBaseFileName(phantasus.Util.getFileName(this.options.dataset)),
        extension: ext
      });
      if (colorScheme == null) {
        colorScheme = {
          type: 'relative'
        };
      }
      this.options.colorScheme = colorScheme;
      var name = this.project.getFullDataset().getName();
      if (ext === 'maf' && !this.options.rowSortBy) {
        var sortKeys = [];
        if (this.project.getFullDataset().getRowMetadata().getByName(
            'order')) {
          sortKeys.push(new phantasus.SortKey('order',
            phantasus.SortKey.SortOrder.ASCENDING));
        }
        sortKeys.push(new phantasus.SortKey('id',
          phantasus.SortKey.SortOrder.ASCENDING));
        this.project.setRowSortKeys(sortKeys, false);

      }
      if (phantasus.DatasetUtil.getSeriesIndex(this.project.getFullDataset(), 'allelic_fraction') !== -1) {
        this.options.sizeBy = {
          seriesName: 'allelic_fraction',
          min: 0,
          max: 1
        };
      }

    }
    if (this.options.parent && this.options.inheritFromParent) {
      this.heatmap.setPropertiesFromParent(this.options.parent.heatmap);
    }
    if (this.options.parent && this.options.inheritFromParent
      && !colorSchemeSpecified) {
      heatmap.setColorScheme(this.options.parent.heatmap.getColorScheme().copy(this.project));
    } else {
      heatmap.setColorScheme(new phantasus.HeatMapColorScheme(
        this.project, this.options.colorScheme));
      if (this.options.dataset.getRowMetadata().getByName('Source') != null) {
        // separate color scheme for each source file
        var sourcesSet = phantasus.VectorUtil.getSet(this.options.dataset.getRowMetadata().getByName('Source'));
        this.heatmap.getColorScheme().setSeparateColorSchemeForRowMetadataField('Source');
        sourcesSet.forEach(function (source) {
          _this.autoDisplay({
            extension: phantasus.Util.getExtension(source),
            filename: '' + source
          });
        });
      }
    }

    if (this.options.sizeBy) {
      heatmap.getColorScheme().getSizer().setSeriesName(
        this.options.sizeBy.seriesName);
      heatmap.getColorScheme().getSizer().setMin(
        this.options.sizeBy.min);
      heatmap.getColorScheme().getSizer().setMax(
        this.options.sizeBy.max);
    }

    this.updateDataset();

    // tabOpened is inherited by child heat maps
    if (this.options.tabOpened) {
      this.options.tabOpened(this);
      this.updateDataset();
    }
    // renderReady is only called once for the parent heat map
    if (this.options.renderReady) {
      this.options.renderReady(this);
      this.updateDataset();
    }

    if (this.options.rowSize === 'fit') {
      this.heatmap.getRowPositions().setSize(this.getFitRowSize());
      this.revalidate({
        paint: false
      });
    }
    if (this.options.columnSize === 'fit') {
      this.heatmap.getColumnPositions().setSize(
        this.getFitColumnSize());
      this.revalidate({
        paint: false
      });

    }
    if (this.options.rowColorModel) {
      this.getProject().getRowColorModel().fromJSON(this.options.rowColorModel);
    }
    if (this.options.columnColorModel) {
      this.getProject().getColumnColorModel().fromJSON(this.options.columnColorModel);
    }
    if (this.options.rowShapeModel) {
      this.getProject().getRowShapeModel().fromJSON(this.options.rowShapeModel);
    }
    if (this.options.columnShapeModel) {
      this.getProject().getColumnShapeModel().fromJSON(this.options.columnShapeModel);
    }
    if (this.options.rowFontModel) {
      this.getProject().getRowFontModel().fromJSON(this.options.rowFontModel);
    }
    if (this.options.columnFontModel) {
      this.getProject().getColumnFontModel().fromJSON(this.options.columnFontModel);
    }
    if (this.options.rowSize === 'fit' || this.options.columnSize === 'fit') {
      // note that we have to revalidate twice because column sizes are
      // dependent on row sizes and vice versa
      if (this.options.columnSize === 'fit') {
        this.heatmap.getColumnPositions().setSize(
          this.getFitColumnSize());
        this.revalidate({
          paint: false
        });
      }
      if (this.options.rowSize === 'fit') {
        this.heatmap.getRowPositions().setSize(this.getFitRowSize());
        this.revalidate({
          paint: false
        });
      }
      this.paintAll({
        paintRows: true,
        paintColumns: true,
        invalidateRows: true,
        invalidateColumns: true
      });
    }

    this.options.parent = null; // avoid memory leak
    this.$tipFollow = $('<div style="display:none;"' +
      ' class="phantasus-tip-inline"></div>');
    this.$tipFollow.appendTo(this.$parent);

    this.$tipInfoWindow = $('<div class="phantasus-tip-dialog"></div>');
    this.$tipInfoWindow.appendTo(this.$parent);

    if (!phantasus.Util.isHeadless()) {
      this.$tipInfoWindow.dialog({
        close: function (event, ui) {
          if (!_this._togglingInfoWindow) {
            _this.toggleInfoWindow();
          }
        },
        autoOpen: false,
        width: 220,
        height: 280,
        minHeight: 38,
        minWidth: 10,
        collision: 'fit',
        position: {
          my: 'right-30 bottom',
          at: 'right top',
          of: this.$parent
        },
        title: 'Info'
      });
      this.setTooltipMode(this.options.tooltipMode);
    }

    this.getProject().on(
      'rowFilterChanged columnFilterChanged rowGroupByChanged columnGroupByChanged rowSortOrderChanged columnSortOrderChanged datasetChanged',
      function (e) {
        if (e.type === 'datasetChanged') { // remove
            // tracks
            // that are no
            // longer in the
            // dataset

            var dataset = _this.getProject().getFullDataset();
            for (var i = 0; i < _this.rowTracks.length; i++) {
              var track = _this.rowTracks[i];
              if (!dataset.getRowMetadata().getByName(
                  track.getName())) {
                _this.removeTrack(track.getName(),
                  false);
                i--;
              }
            }
            for (var i = 0; i < _this.columnTracks.length; i++) {
              var track = _this.columnTracks[i];
              if (!dataset.getColumnMetadata().getByName(
                  track.getName())) {
                _this.removeTrack(track.getName(),
                    true);
                i--;
              }
            }

          }

          _this.updateDataset();
          _this.revalidate();
        });

    this.getProject().on('trackChanged', function (e) {
      var columns = e.columns;
      _.each(e.vectors, function (v, i) {
        var index = _this.getTrackIndex(v.getName(), columns);
        if (index === -1) {
          _this.addTrack(v.getName(), columns, e.display[i]);
        } else {
          // repaint
          var track = _this.getTrackByIndex(index, columns);
          var display = e.display[i];
          if (display) {
            track.settingFromConfig(display);
          }
          track.setInvalid(true);
        }
      });
      _this.revalidate();
    });
    this.getProject().on('rowTrackRemoved', function (e) {
      _this.removeTrack(e.vector.getName(), false);
      _this.revalidate();
    });
    this.getProject().on('columnTrackRemoved', function (e) {
      _this.removeTrack(e.vector.getName(), true);
      _this.revalidate();
    });
    this.getProject().getRowSelectionModel().on(
      'selectionChanged',
      function () {
        // repaint tracks that indicate selection
        for (var i = 0; i < _this.columnTracks.length; i++) {
          var track = _this.columnTracks[i];
          if (track.getFullVector().getProperties().get(phantasus.VectorKeys.RECOMPUTE_FUNCTION_SELECTION)) {
            var selectedDataset = _this.getProject().getSelectedDataset({
              selectedRows: true,
              selectedColumns: false,
              emptyToAll: false
            });
            var vector = selectedDataset.getColumnMetadata().getByName(track.getName());
            var f = phantasus.VectorUtil.jsonToFunction(vector, phantasus.VectorKeys.FUNCTION);
            if (typeof f === 'function') {
              // iterate over each column
              var view = new phantasus.DatasetColumnView(selectedDataset);
              // TODO only set values that are currently visible
              for (var j = 0, size = vector.size(); j < size; j++) {
                view.setIndex(j);
                vector.setValue(j, f(view, selectedDataset, j));
              }
              track.setInvalid(true);
              track.repaint();
            }
          }
        }
        _this.verticalSearchBar.update();
        _this.heatmap.updateRowSelectionCache();
        _this.paintAll({
          paintRows: true,
          paintColumns: false,
          invalidateRows: false,
          invalidateColumns: false
        });
      });
    this.getProject().getColumnSelectionModel().on('selectionChanged',
      function () {
        _this.horizontalSearchBar.update();
        _this.heatmap.updateColumnSelectionCache();
        _this.paintAll({
          paintRows: false,
          paintColumns: true,
          invalidateRows: false,
          invalidateColumns: false
        });
      });

    this.pasteListener = function (e) {
      if (_this.isActiveComponent()) {
        var text = e.originalEvent.clipboardData.getData('text/plain');
        if (text != null && text.length > 0) {
          e.preventDefault();
          e.stopPropagation();
          // open a file from clipboard
          var url;
          if (text.indexOf('http') === 0) {
            url = text;
          } else {
            var blob = new Blob([text], {type: 'text/plain'});
            url = URL.createObjectURL(blob);
          }
          phantasus.HeatMap.showTool(
            new phantasus.OpenFileTool({
              file: url
            }), _this);
        }
      }
    };
    this.beforeCopyListener = function (e) {
      if (_this.isActiveComponent()) {
        e.preventDefault();
      }

    };
    this.copyListener = function (ev) {
      if (_this.isActiveComponent()) {
        var activeComponent = _this.getActiveComponent();
        var project = _this.project;
        if (activeComponent === 'heatMap' || ev.shiftKey) {
          // copy selected text or image
          // var text = _this.getSelectedElementsText();
          // if (text !== '') {
          // 	ev.originalEvent.clipboardData.setData(
          // 		'text/plain', text);
          // 	return;
          // }
          var bounds = _this.getTotalSize();
          var height = bounds.height;
          var width = bounds.width;
          var canvas = $('<canvas></canvas>')[0];
          canvas.height = height;
          canvas.width = width;
          var context = canvas.getContext('2d');
          _this.snapshot(context);
          var url = canvas.toDataURL();
          ev.originalEvent.clipboardData.setData(
            'text/html',
            '<img src="' + url + '">');
          ev.preventDefault();
          ev.stopImmediatePropagation();
          return;
        }
        // copy all selected rows and columns
        var dataset = project.getSelectedDataset({
          emptyToAll: false
        });
        var rowsSelected = dataset.getRowCount() > 0;
        var columnsSelected = dataset.getColumnCount() > 0;
        var columnMetadata = dataset.getColumnMetadata();
        var rowMetadata = dataset.getRowMetadata();
        // only copy visible tracks
        var visibleColumnFields = _this.getVisibleTrackNames(true);
        var columnFieldIndices = [];
        _.each(visibleColumnFields, function (name) {
          var index = phantasus.MetadataUtil.indexOf(
            columnMetadata, name);
          if (index !== -1) {
            columnFieldIndices.push(index);
          }
        });
        columnMetadata = new phantasus.MetadataModelColumnView(
          columnMetadata, columnFieldIndices);
        var rowMetadata = dataset.getRowMetadata();
        // only copy visible tracks
        var visibleRowFields = _this.getVisibleTrackNames(false);
        var rowFieldIndices = [];
        _.each(visibleRowFields, function (name) {
          var index = phantasus.MetadataUtil.indexOf(
            rowMetadata, name);
          if (index !== -1) {
            rowFieldIndices.push(index);
          }
        });
        rowMetadata = new phantasus.MetadataModelColumnView(
          rowMetadata, rowFieldIndices);
        var text = [];

        if (rowsSelected && columnsSelected) { // copy
          // as
          // gct
          // 1.3
          text = new phantasus.GctWriter().write(dataset);
        } else {
          var text = [];
          var model = rowsSelected ? rowMetadata
            : columnMetadata;
          for (var i = 0, count = model.getItemCount(); i < count; i++) {
            for (var j = 0, nfields = model.getMetadataCount(); j < nfields; j++) {
              var v = model.get(j);
              if (j > 0) {
                text.push('\t');
              }
              text.push(phantasus.Util.toString(v.getValue(i)));
            }
            text.push('\n');
          }
          text = text.join('');
        }
        ev.originalEvent.clipboardData.setData(
          'text/plain', text);
        ev.preventDefault();
        ev.stopImmediatePropagation();

      }
    };

    if (typeof window !== 'undefined') {
      $(window)
        .on('paste.phantasus', this.pasteListener)
        .on('beforecopy.phantasus', this.beforeCopyListener)
        .on('copy.phantasus', this.copyListener);
    }
    if (this.options.keyboard && !phantasus.Util.isHeadless()) {
      new phantasus.HeatMapKeyListener(this);
    }
    if (this.options.symmetric) {
      this.getProject().setSymmetric(this);
    }
    var dragStartScrollTop;
    var dragStartScrollLeft;
    var panstartMousePosition;
    this.hammer = phantasus.Util.hammer(_this.heatmap.canvas, ['pan', 'pinch', 'tap', 'swipe']).on('swipe', this.swipe = function (event) {
      event.preventDefault();
    }).on('panend', this.panend = function (event) {
      _this.panning = false;
      if (panstartMousePosition) {
        panstartMousePosition = null;
        _this.heatmap.setSelectionBox(null);
        _this.heatmap.repaint();
      }
      event.preventDefault();
    }).on('panmove', this.panmove = function (event) {
      if (panstartMousePosition) {
        var pos = phantasus.CanvasUtil.getMousePosWithScroll(event.target, event,
          _this.scrollLeft(), _this.scrollTop());
        var rowIndex = _this.heatmap.getRowPositions().getIndex(pos.y, false);
        var columnIndex = _this.heatmap.getColumnPositions().getIndex(pos.x, false);
        _this.updatingScroll = false;
        _this.heatmap.setSelectionBox({
          y: [panstartMousePosition.rowIndex, rowIndex],
          x: [panstartMousePosition.columnIndex, columnIndex]
        });
        var rowIndices = new phantasus.Set();
        for (var i = Math.min(panstartMousePosition.rowIndex, rowIndex),
               end = Math.max(panstartMousePosition.rowIndex, rowIndex); i <= end; i++) {
          rowIndices.add(i);
        }
        var columnIndices = new phantasus.Set();
        for (var i = Math.min(panstartMousePosition.columnIndex, columnIndex),
               end = Math.max(panstartMousePosition.columnIndex, columnIndex); i <= end; i++) {
          columnIndices.add(i);
        }
        _this.project.getRowSelectionModel().setViewIndices(rowIndices, true);
        _this.project.getColumnSelectionModel().setViewIndices(columnIndices, true);
        // _this.heatmap.repaint(); don't need to repaint as setViewIndices triggers repaint
      } else {
        _this.updatingScroll = true; // avoid infinite paints
        var rows = false;
        var columns = false;
        if (event.deltaY !== 0) {
          var pos = dragStartScrollTop + event.deltaY;
          _this.scrollTop(pos);
          rows = true;
        }
        if (event.deltaX !== 0) {
          var pos = dragStartScrollLeft + event.deltaX;
          _this.scrollLeft(pos);
          columns = true;
        }
        _this.updatingScroll = false;
        if (rows || columns) {
          _this.paintAll({
            paintRows: rows,
            paintColumns: rows,
            invalidateRows: rows,
            invalidateColumns: columns
          });
        }
      }
      event.preventDefault();
    }).on('panstart', this.panstart = function (event) {
      _this.panning = true; // don't draw inline tooltips when panning
      _this.project.setHoverRowIndex(-1);
      _this.project.setHoverColumnIndex(-1);

      if (event.srcEvent.shiftKey) {
        var pos = phantasus.CanvasUtil.getMousePosWithScroll(event.target, event,
          _this.scrollLeft(), _this.scrollTop());
        panstartMousePosition = {
          rowIndex: _this.heatmap.getRowPositions().getIndex(pos.y, false),
          columnIndex: _this.heatmap.getColumnPositions().getIndex(pos.x, false)
        };
      } else {
        panstartMousePosition = null;
        dragStartScrollTop = _this.scrollTop();
        dragStartScrollLeft = _this.scrollLeft();
      }
      event.preventDefault();
    }).on(
      'tap',
      this.tap = function (event) {
        // var commandKey = phantasus.Util.IS_MAC ? event.srcEvent.metaKey
        //   : event.srcEvent.ctrlKey;
        if (phantasus.Util.IS_MAC && event.srcEvent.ctrlKey) { // right-click
          // on
          // Mac
          return;
        }
        _this.project.getRowSelectionModel().setViewIndices(new phantasus.Set(), true);
        _this.project.getColumnSelectionModel().setViewIndices(new phantasus.Set(), true);
        // var position = phantasus.CanvasUtil
        // .getMousePosWithScroll(event.target, event,
        //   _this.scrollLeft(), _this
        //   .scrollTop());
        // var rowIndex = _this.heatmap.getRowPositions()
        // .getIndex(position.y, false);
        // var columnIndex = _this.heatmap
        // .getColumnPositions().getIndex(position.x,
        //   false);
        // _this.project.getElementSelectionModel().click(
        //   rowIndex, columnIndex,
        //   event.srcEvent.shiftKey || commandKey);
      }).on(
      'pinch',
      this.pinch = function (event) {
        var scale = event.scale;
        _this.heatmap.getRowPositions().setSize(13 * scale);
        _this.heatmap.getColumnPositions().setSize(
          13 * scale);
        var reval = {};
        if (_this.project.getHoverRowIndex() !== -1) {
          reval.scrollTop = this.heatmap.getRowPositions().getPosition(
            this.project.getHoverRowIndex());
        }
        if (_this.project.getHoverColumnIndex() !== -1) {
          reval.scrollLeft = this.heatmap.getColumnPositions().getPosition(
            this.project.getHoverColumnIndex());
        }

          _this.revalidate(reval);
          event.preventDefault();
        });
    var heatMapMouseMoved = function (event) {
      var mouseI, mouseJ;
      if (event.type === 'mouseout') {
        mouseI = -1;
        mouseJ = -1;
      } else {
        var position = phantasus.CanvasUtil.getMousePosWithScroll(
          event.target, event, _this.scrollLeft(), _this.scrollTop());
        mouseI = _this.heatmap.getRowPositions().getIndex(position.y,
          false);
        mouseJ = _this.heatmap.getColumnPositions().getIndex(
          position.x, false);
      }
      _this.setMousePosition(mouseI, mouseJ, {
        event: event
      });
    };
    $(_this.heatmap.canvas).on('mouseout', heatMapMouseMoved).on(
      'mousemove', heatMapMouseMoved);
    // tools to run at load time
    _.each(this.options.tools, function (item) {
      var action = _this.getActionManager().getAction(item.name);
      if (action == null) {
        // console.log(item.name + ' not found.');
      } else {

        var actionGui = action.gui();
        var gui = actionGui.gui(_this.getProject());
        var formBuilder = new phantasus.FormBuilder();
        _.each(gui, function (item) {
          formBuilder.append(item);
        });
        var input = {};
        _.each(gui, function (item) {
          input[item.name] = formBuilder.getValue(item.name);
        });
        if (item.params) {
          // overide default values
          for (var key in item.params) {
            input[key] = item.params[key];
          }
        }

        actionGui.execute({
          heatMap: _this,
          project: _this.getProject(),
          input: input
        });

      }

    });
  },
  setMousePosition: function (i, j, options) {
    this.mousePositionOptions = options;
    var updateColumns = this.project.getHoverColumnIndex() !== j;
    var updateRows = this.project.getHoverRowIndex() !== i;
    if (updateColumns || updateRows) {
      if (!this.panning) {
        this.project.setHoverRowIndex(i);
        this.project.setHoverColumnIndex(j);
      }
      this.setToolTip(i, j, options);
      this.paintAll({
        paintRows: updateRows,
        paintColumns: updateColumns,
        invalidateRows: false,
        invalidateColumns: false
      });
    } else {
      this._updateTipFollowPosition(options);

    }
    // else if (this.options.tooltipMode === 2 &&
    // (this.project.getHoverColumnIndex() !== -1 || this.project
    // .getHoverRowIndex() !== -1)) {
    //
    // }
    this.trigger('change', {
      name: 'setMousePosition',
      source: this,
      arguments: arguments
    });
  },
  getContentEl: function () {
    return this.$content;
  },
  focus: function () {
    var scrollTop = document.body.scrollTop;
    this.$tabPanel.focus();
    document.body.scrollTop = scrollTop;
  },
  getFocusEl: function () {
    return this.$tabPanel;
  },
  /**
   Set where the tooltip is shown
   @param mode 0 is formula bar, 1 is dialog, -1 is no tooltip
   */

  setTooltipMode: function (mode) {
    this._togglingInfoWindow = true;
    this.options.tooltipMode = mode;
    this.$tipInfoWindow.html('');
    this.toolbar.$tip.html('');
    this.$tipFollow.html('').css({
      display: 'none'
    });
    this.toolbar.$tip.css('display', mode === 0 ? '' : 'none');
    this.setToolTip(-1, -1);
    if (this.options.tooltipMode === 1) {
      this.$tipInfoWindow.dialog('open');
    } else {
      this.$tipInfoWindow.dialog('close');
    }
    this._togglingInfoWindow = false;
  },
  toggleInfoWindow: function () {
    this.setTooltipMode(this.options.tooltipMode == 1 ? 0 : 1);
  },
  _setTipText: function (tipText, tipFollowText, options) {
    if (this.options.tooltipMode === 0) {
      this.toolbar.$tip.html(tipText.join(''));
    } else if (this.options.tooltipMode === 1) {
      this.$tipInfoWindow.html(tipText.join(''));
    }

    if (tipFollowText != null) {
      this.tipFollowHidden = false;
      this.$tipFollow.html(tipFollowText);
      this._updateTipFollowPosition(options);
    } else {
      this.tipFollowHidden = true;
      this.$tipFollow.empty().css({
        display: 'none'
      });
    }
    this.trigger('change', {
      name: 'setToolTip',
      source: this,
      arguments: arguments
    });
  },
  setToolTip: function (rowIndex, columnIndex, options) {
    options = options || {};
    if (this.options.showSeriesNameInTooltip) {
      options.showSeriesNameInTooltip = true;
    }
    if (this.options.tooltipSeriesIndices) {
      options.tooltipSeriesIndices = this.options.tooltipSeriesIndices;
    }
    if (options.heatMapLens) {
      var maxSelectedCount = 20;
      // don't draw lens if currently visible
      // row lens
      var $wrapper = $('<div></div>');
      var wrapperHeight = 0;
      var wrapperWidth = 0;
      var found = false;
      var inline = [];
      var indicesForLens = [];
      // only draw heat map lens if less than maxSelectedCount indices selected
      if (rowIndex != null && rowIndex.length > 0) {
        for (var hoverIndex = 0; hoverIndex < rowIndex.length; hoverIndex++) {
          var row = rowIndex[hoverIndex];
          if (row >= 0 && (row >= this.heatmap.lastPosition.bottom || row < this.heatmap.lastPosition.top)) {
            indicesForLens.push(row);
          } else {
            inline.push(row);
          }
        }
        if (indicesForLens.length < maxSelectedCount) {
          for (var hoverIndex = 0; hoverIndex < indicesForLens.length; hoverIndex++) {
            var row = indicesForLens[hoverIndex];
            var heatMapWidth = this.heatmap.getUnscaledWidth();
            var top = row; // Math.max(0, rowIndex - 1);
            var bottom = row + 1; //Math.min(rowIndex + 1, this.heatmap.rowPositions.getLength());
            var startPix = this.heatmap.rowPositions.getPosition(top);
            var endPix = startPix + this.heatmap.rowPositions.getItemSize(top);
            var heatMapHeight = endPix - startPix;
            var canvas = phantasus.CanvasUtil.createCanvas();
            var trackWidth = 0;
            for (var i = 0, ntracks = this.rowTracks.length; i < ntracks; i++) {
              var track = this.rowTracks[i];
              if (track.isVisible()) {
                trackWidth += track.getUnscaledWidth();
              }
            }

            var canvasWidth = trackWidth + heatMapWidth + phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS;
            canvas.width = canvasWidth * phantasus.CanvasUtil.BACKING_SCALE;
            canvas.style.width = canvasWidth + 'px';
            canvas.height = heatMapHeight * phantasus.CanvasUtil.BACKING_SCALE;
            canvas.style.height = heatMapHeight + 'px';
            var context = canvas.getContext('2d');
            phantasus.CanvasUtil.resetTransform(context);
            context.save();
            context.translate(-this.heatmap.lastClip.x, -startPix);
            context.rect(this.heatmap.lastClip.x, startPix, this.heatmap.lastClip.width, this.heatmap.lastClip.height);
            context.clip();
            this.heatmap._draw({
              left: this.heatmap.lastPosition.left,
              right: this.heatmap.lastPosition.right,
              top: top,
              bottom: bottom,
              context: context
            });
            context.restore();
            context.translate(heatMapWidth + phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS, -startPix);
            trackWidth = 0;
            for (var i = 0, ntracks = this.rowTracks.length; i < ntracks; i++) {
              var track = this.rowTracks[i];
              if (track.isVisible()) {
                context.save();
                context.translate(trackWidth, 0);
                context.rect(0, startPix, track.getUnscaledWidth(), track.lastClip.height);
                context.clip();
                track._draw({
                  start: top,
                  end: bottom,
                  vector: track.getVector(),
                  context: context,
                  availableSpace: track.getUnscaledWidth()
                });
                context.restore();
                trackWidth += track.getUnscaledWidth();
              }
            }
            $(canvas).appendTo($wrapper);
            canvas.style.top = wrapperHeight + 'px';
            wrapperHeight += parseFloat(canvas.style.height);
            wrapperWidth = parseFloat(canvas.style.width);

          }
        }

        if (indicesForLens.length > 0) {
          if (indicesForLens.length < maxSelectedCount) {
            $wrapper.css({
              height: wrapperHeight,
              width: wrapperWidth
            });

            var rect = this.$parent[0].getBoundingClientRect();
            this.$tipFollow.html($wrapper).css({
              display: '',
              left: Math.round(parseFloat(this.heatmap.canvas.style.left) - 1) + 'px',
              top: (options.event.clientY - rect.top - wrapperHeight / 2) + 'px'
            });
          } else {
            this.$tipFollow.html('');
          }
          return;
        } else {
          var tipText = [];
          var tipFollowText = [];
          if (inline.length < maxSelectedCount) {
            for (var hoverIndex = 0; hoverIndex < inline.length; hoverIndex++) {
              this.tooltipProvider(this, inline[hoverIndex], -1,
                options, this.options.tooltipMode === 0 ? '&nbsp;&nbsp;&nbsp;'
                  : '<br />', false, tipText);
              if (this.options.inlineTooltip) {
                this.tooltipProvider(this, inline[hoverIndex], -1,
                  options, '<br />', true, tipFollowText);

              }
            }
          }
          var text = tipFollowText.join('');
          this._setTipText(tipText, text.length === 0 ? null : '<span style="max-width:400px;">' + text + '</span>', options);
        }
      }
      if (columnIndex != null && columnIndex.length > 0) {
        for (var hoverIndex = 0; hoverIndex < columnIndex.length; hoverIndex++) {
          var column = columnIndex[hoverIndex];
          if (column >= 0 && (column >= this.heatmap.lastPosition.right || column < this.heatmap.lastPosition.left)) {
            indicesForLens.push(column);
          } else {
            inline.push(column);
          }
        }

        if (indicesForLens.length < maxSelectedCount) {
          for (var hoverIndex = 0; hoverIndex < indicesForLens.length; hoverIndex++) {
            var column = indicesForLens[hoverIndex];
            var heatMapHeight = this.heatmap.getUnscaledHeight();
            var left = column; // Math.max(0, rowIndex - 1);
            var right = column + 1; //Math.min(rowIndex + 1, this.heatmap.rowPositions.getLength());
            var startPix = this.heatmap.columnPositions.getPosition(left);
            var endPix = startPix + this.heatmap.columnPositions.getItemSize(left);
            var heatMapWidth = endPix - startPix;
            var canvas = phantasus.CanvasUtil.createCanvas();
            var trackHeight = 0;
            for (var i = 0, ntracks = this.columnTracks.length; i < ntracks; i++) {
              var track = this.columnTracks[i];
              if (track.isVisible()) {
                trackHeight += track.getUnscaledHeight();
              }
            }
            var canvasHeight = trackHeight + heatMapHeight + phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS;
            canvas.width = heatMapWidth * phantasus.CanvasUtil.BACKING_SCALE;
            canvas.style.width = heatMapWidth + 'px';
            canvas.height = canvasHeight * phantasus.CanvasUtil.BACKING_SCALE;
            canvas.style.height = canvasHeight + 'px';
            var context = canvas.getContext('2d');
            phantasus.CanvasUtil.resetTransform(context);
            context.translate(-startPix, 0);
            context.save();
            context.rect(startPix, trackHeight + phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS,
              this.heatmap.lastClip.width, this.heatmap.lastClip.height + trackHeight +
              phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS);
            context.clip();
            context.translate(0, trackHeight + phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS - this.heatmap.lastClip.y);

            this.heatmap._draw({
              top: this.heatmap.lastPosition.top,
              bottom: this.heatmap.lastPosition.bottom,
              left: left,
              right: right,
              context: context
            });
            context.restore();
            trackHeight = 0;
            for (var i = 0, ntracks = this.columnTracks.length; i < ntracks; i++) {
              var track = this.columnTracks[i];
              if (track.isVisible()) {
                context.save();
                context.translate(0, trackHeight);
                context.rect(startPix, 0, track.lastClip.width, track.getUnscaledHeight());
                context.clip();
                track._draw({
                  start: left,
                  end: right,
                  vector: track.getVector(),
                  context: context,
                  availableSpace: track.getUnscaledHeight(),
                  clip: {
                    x: track.lastClip.x,
                    y: track.lastClip.y
                  }
                });
                context.restore();
                trackHeight += track.getUnscaledHeight();
              }
            }
            canvas.style.left = wrapperWidth + 'px';
            wrapperWidth += parseFloat(canvas.style.width);
            wrapperHeight = parseFloat(canvas.style.height);
            $(canvas).appendTo($wrapper);
          }
        }
        if (indicesForLens.length > 0) {
          if (indicesForLens.length < maxSelectedCount) {
            $wrapper.css({
              height: wrapperHeight,
              width: wrapperWidth
            });

            var rect = this.$parent[0].getBoundingClientRect();
            this.$tipFollow.html($wrapper).css({
              top: parseFloat(this.heatmap.canvas.style.top) - trackHeight - phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS - 1,
              left: (options.event.clientX - rect.left) - (wrapperWidth / 2),
              display: ''
            });

          } else {
            this.$tipFollow.html('');
          }
          return;
        } else {
          var tipText = [];
          var tipFollowText = [];
          if (inline.length < maxSelectedCount) {
            for (var hoverIndex = 0; hoverIndex < inline.length; hoverIndex++) {
              this.tooltipProvider(this, -1, inline[hoverIndex],
                options, this.options.tooltipMode === 0 ? '&nbsp;&nbsp;&nbsp;'
                  : '<br />', false, tipText);
              if (this.options.inlineTooltip) {
                this.tooltipProvider(this, -1, inline[hoverIndex],
                  options, '<br />', true, tipFollowText);
              }
            }
          }
          var text = tipFollowText.join('');
          this._setTipText(tipText, text === '' ? null : '<span style="max-width:400px;">' + text + '</span>', options);
        }
      }

      // column lens

    }

    // tooltipMode=0 top, 1=window, 2=inline
    var tipText = [];
    this.tooltipProvider(this, rowIndex, columnIndex,
      options, this.options.tooltipMode === 0 ? '&nbsp;&nbsp;&nbsp;'
        : '<br />', false, tipText);

    var text = [];
    var customToolTip = false;
    var $tipFollowText;
    if (!this.panning) {
      var tipFollowText = [];
      if (this.options.inlineTooltip) {
        this.tooltipProvider(this, rowIndex, columnIndex,
          options, '<br />', true, tipFollowText);

        if (this.options.tooltip && rowIndex !== -1 && columnIndex !== -1) {
          tipFollowText.push('<div data-name="tip"></div>');
        }
      }

      text = tipFollowText.join('');
      $tipFollowText = $('<span style="max-width:400px;">' + text + '</span>');

      // tooltip callback
      if (this.options.tooltip && rowIndex !== -1 && columnIndex !== -1) {
        this.options.tooltip(this, rowIndex, columnIndex, $tipFollowText.find('[data-name=tip]'));
        customToolTip = true;
      }

    }
    this._setTipText(tipText, text.length > 0 || customToolTip ? $tipFollowText : null, options);

  }
  ,
  _updateTipFollowPosition: function (options) {
    if (this.tipFollowHidden) {
      return;
    }
    // top, bottom are negative when scrolled
    var parentRect = this.$parent[0].getBoundingClientRect();
    var tipRect = this.$tipFollow[0].getBoundingClientRect();
    var tipWidth = tipRect.width;
    var tipHeight = tipRect.height;
    var offset = 10;
    var left = options.event.clientX - parentRect.left + offset;
    var top = options.event.clientY - parentRect.top + offset;
    // default is bottom-right
    var scrollBarSize = 18;
    if ((left + tipWidth) >= ( parentRect.right - parentRect.left - scrollBarSize)) { // offscreen
      // right, place tip on
      // left
      left = options.event.clientX - parentRect.left - offset - tipWidth;
    }
    if ((top + tipHeight) >= (parentRect.bottom - parentRect.top - scrollBarSize)) { // offscreen
      // bottom,
      // place tip
      // on top
      top = options.event.clientY - parentRect.top - offset - tipHeight;
    }
    this.$tipFollow.css({
      left: left + 'px',
      top: top + 'px',
      display: ''
    });

  }
  ,
  setTrackVisibility: function (tracks) {
    var _this = this;
    _.each(tracks, function (track) {
      var existingTrack = _this.getTrack(track.name, track.isColumns);
      if (track.visible && existingTrack != null
        && _.keys(existingTrack.settings).length === 0) {
        existingTrack.settingFromConfig('Text');
      }
      _this.setTrackVisible(track.name, track.visible, track.isColumns);
    });
    this.revalidate();
    this.trigger('change', {
      name: 'setTrackVisibility',
      source: this,
      arguments: arguments
    });
  }
  ,
  setTrackVisible: function (name, visible, isColumns) {
    var trackIndex = this.getTrackIndex(name, isColumns);
    if (trackIndex === -1) { // not currently visible
      if (!visible) {
        return;
      }
      this.addTrack(name, isColumns);
    } else {
      var track = isColumns ? this.columnTracks[trackIndex]
        : this.rowTracks[trackIndex];
      var header = isColumns ? this.columnTrackHeaders[trackIndex]
        : this.rowTrackHeaders[trackIndex];
      if (track.isVisible() !== visible) {
        track.setVisible(visible);
        header.setVisible(visible);
      } else {
        return;
      }
    }
    this.trigger('change', {
      name: 'setTrackVisible',
      source: this,
      arguments: arguments
    });
  },
  addTrack: function (name, isColumns, renderSettings, trackIndex) {
    if (name === undefined) {
      throw 'Name not specified';
    }

    var tracks = isColumns ? this.columnTracks : this.rowTracks;
    var headers = isColumns ? this.columnTrackHeaders : this.rowTrackHeaders;
    // see if already exists
    var existingIndex = this.getTrackIndex(name, isColumns);
    if (existingIndex !== -1) {
      return tracks[existingIndex];
    }
    if (renderSettings == null) {
      var metadata = isColumns ? this.project.getFullDataset().getColumnMetadata() : this.project.getFullDataset().getRowMetadata();
      renderSettings = phantasus.VectorUtil.getDataType(metadata.getByName(name)) === '[number]' ? {display: ['bar']}
        : {display: ['text']};
    }

    var positions = isColumns ? this.heatmap.getColumnPositions() : this.heatmap.getRowPositions();
    var track = new phantasus.VectorTrack(this.project, name, positions, isColumns, this);
    track.settingFromConfig(renderSettings);
    track.appendTo(this.$parent);
    var header = new phantasus.VectorTrackHeader(this.project, name, isColumns,
      this);
    header.appendTo(this.$parent);
    track._selection = new phantasus.TrackSelection(track, positions,
      isColumns ? this.project.getColumnSelectionModel() : this.project.getRowSelectionModel(),
      isColumns, this);
    if (trackIndex != null && trackIndex >= 0) {
      tracks.splice(trackIndex, 0, track);
      headers.splice(trackIndex, 0, header);
    } else {
      tracks.push(track);
      headers.push(header);
    }
    return track;
  }
  ,
  addPopup: function (item) {
    if (!this.popupItems) {
      this.popupItems = [];
    }
    this.popupItems.push(item);
  }
  ,
  getPopupItems: function () {
    return this.popupItems || [];
  }
  ,
  removeTrack: function (name, isColumns) {
    var index = this.getTrackIndex(name, isColumns);
    var tracks = isColumns ? this.columnTracks : this.rowTracks;
    if (isNaN(index) || index < 0 || index >= tracks.length) {
      // console.log('removeTrack: ' + name + ' not found.');
      return;
    }

    var headers = isColumns ? this.columnTrackHeaders
      : this.rowTrackHeaders;
    var track = tracks[index];
    var header = headers[index];
    track.dispose();
    header.dispose();
    tracks.splice(index, 1);
    headers.splice(index, 1);
    this.trigger('change', {
      name: 'removeTrack',
      source: this,
      arguments: arguments
    });
  }
  ,
  updateDataset: function () {
    var dataset = this.project.getSortedFilteredDataset();
    this.verticalSearchBar.update();
    this.horizontalSearchBar.update();
    this.heatmap.setDataset(dataset);
    this.heatmap.getRowPositions().setSpaces(phantasus.HeatMap.createGroupBySpaces(dataset, this.project.getGroupRows(),
      this.rowGapSize, false));
    this.heatmap.getColumnPositions()
      .setSpaces(phantasus.HeatMap.createGroupBySpaces(dataset, this.project.getGroupColumns(), this.columnGapSize, true));
    this.trigger('change', {
      name: 'updateDataset',
      source: this,
      arguments: arguments
    });
  }
  ,
  zoom: function (isZoomIn, options) {
    options = $.extend({}, {
      rows: true,
      columns: true
    }, options);
    if (isZoomIn) {
      if (options.rows) {
        this.heatmap.getRowPositions().setSize(
          this.heatmap.getRowPositions().getSize() * 1.5);
      }
      if (options.columns) {
        this.heatmap.getColumnPositions().setSize(
          this.heatmap.getColumnPositions().getSize() * 1.5);
      }
    } else {
      if (options.rows) {
        this.heatmap.getRowPositions().setSize(
          this.heatmap.getRowPositions().getSize() / 1.5);
      }
      if (options.columns) {
        this.heatmap.getColumnPositions().setSize(
          this.heatmap.getColumnPositions().getSize() / 1.5);
      }
    }
    var reval = {};
    if (options.rows && this.project.getHoverRowIndex() !== -1) {
      reval.scrollTop = this.heatmap.getRowPositions().getPosition(
        this.project.getHoverRowIndex());
    }
    if (options.columns && this.project.getHoverColumnIndex() !== -1) {
      reval.scrollLeft = this.heatmap.getColumnPositions().getPosition(
        this.project.getHoverColumnIndex());
    }
    this.revalidate(reval);
    this.trigger('change', {
      name: 'zoom',
      source: this,
      arguments: arguments
    });
  }
  ,
  getTrackIndex: function (name, isColumns) {
    var tracks = isColumns ? this.columnTracks : this.rowTracks;
    for (var i = 0, length = tracks.length; i < length; i++) {
      if (tracks[i].name !== undefined && tracks[i].name === name) {
        return i;
      }
    }
    return -1;
  }
  ,
  getNumTracks: function (isColumns) {
    return isColumns ? this.columnTracks.length : this.rowTracks.length;
  }
  ,
  moveTrack: function (index, newIndex, isColumns) {
    var tracks = isColumns ? this.columnTracks : this.rowTracks;
    var headers = isColumns ? this.columnTrackHeaders
      : this.rowTrackHeaders;
    var track = tracks[index];
    tracks.splice(index, 1);
    var header = headers[index];
    headers.splice(index, 1);
    tracks.splice(newIndex, 0, track);
    headers.splice(newIndex, 0, header);
    this.trigger('change', {
      name: 'moveTrack',
      source: this,
      arguments: arguments
    });
  }
  ,
  getTrackByIndex: function (index, isColumns) {
    return isColumns ? this.columnTracks[index] : this.rowTracks[index];
  }
  ,
  getTrackHeaderByIndex: function (index, isColumns) {
    return isColumns ? this.columnTrackHeaders[index]
      : this.rowTrackHeaders[index];
  }
  ,
  getTrack: function (name, isColumns) {
    var index = this.getTrackIndex(name, isColumns);
    if (index === -1) {
      return undefined;
    }
    return isColumns ? this.columnTracks[index] : this.rowTracks[index];
  }
  ,
  /**
   * @return true if active element is an ancestor of this heat map.
   */
  isActiveComponent: function () {
    var active = document.activeElement;
    var tagName = active.tagName;
    if (tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA') {
      return false;
    }
    return this.$tabPanel[0].contains(active);
  }
  ,
  /**
   *
   * @return {string} 'rowTrack' if row track is active, 'columnTrack' if column track is active,
   * 'heatMap' if heat map is active.
   */
  getActiveComponent: function () {
    var active = document.activeElement;
    if (active.tagName === 'CANVAS') {
      for (var i = 0, ntracks = this.columnTracks.length; i < ntracks; i++) {
        if (this.columnTracks[i].canvas === active) {
          return 'columnTrack';
        }
      }
      for (var i = 0, ntracks = this.rowTracks.length; i < ntracks; i++) {
        if (this.rowTracks[i].canvas === active) {
          return 'rowTrack';
        }
      }
      if (this.heatmap.canvas === active) {
        return 'heatMap';
      }
    }
    return '';
  },
  dispose: function () {
    //this.$content.remove();
    if (this.project == null) {
      return; // failed to initialize
    }
    this.project.off();
    this.$tipInfoWindow.dialog('destroy');
    this.rowTrackHeaders.forEach(function (header) {
      header.dispose();
    });
    this.columnTrackHeaders.forEach(function (header) {
      header.dispose();
    });
    this.rowTracks.forEach(function (track) {
      track.dispose();
    });
    this.columnTracks.forEach(function (track) {
      track.dispose();
    });
    if (this.rowDendrogram != null) {
      this.rowDendrogram.dispose();
    }
    if (this.columnDendrogram != null) {
      this.columnDendrogram.dispose();
    }
    this.beforeColumnTrackDivider.dispose();
    this.afterRowDendrogramDivider.dispose();
    this.afterVerticalScrollBarDivider.dispose();
    this.hscroll.dispose();
    this.vscroll.dispose();
    this.hammer.off('swipe', this.swipe).off('panmove', this.panmove).off('panstart', this.panstart).off('tap',
      this.tap).off('pinch', this.pinch).off('panend', this.panend);
    this.hammer.destroy();
    if (typeof window !== 'undefined') {
      $(window)
        .off('paste.phantasus', this.pasteListener)
        .off('beforecopy.phantasus', this.beforeCopyListener)
        .off('copy.phantasus', this.copyListener)
        .off('orientationchange.phantasus resize.phantasus', this.resizeListener);
    }
  }
  ,
  getVisibleTrackNames: function (isColumns) {
    return this.getVisibleTracks(isColumns).map(function (track) {
      return track.name;
    });
  },
  getVisibleTracks: function (isColumns) {
    var tracks = isColumns ? this.columnTracks : this.rowTracks;
    return tracks.filter(function (track) {
      return track.isVisible() && !track.getFullVector().getProperties().has(phantasus.VectorKeys.IS_INDEX);
    });
  },
  isShowRowNumber: function () {
    return this.options.showRowNumber;
  },
  setShowRowNumber: function (visible) {
    this.options.showRowNumber = visible;
    if (!visible) {
      this.removeTrack('#', false);
    } else {
      var track = this.addTrack('#', false, {popupEnabled: false, display: ['text']}, 0);
      track.getVector = function (name) {
        var v = new phantasus.AbstractVector('#', this.project.getSortedFilteredDataset().getRowCount());
        v.getProperties().set(phantasus.VectorKeys.FORMATTER, {pattern: 'i'});
        v.getValue = function (index) {
          return index + 1;
        };
        return v;
      };
      track.getFullVector = function () {
        var v = new phantasus.AbstractVector('#', this.project.getFullDataset().getRowCount());
        v.getProperties().set(phantasus.VectorKeys.FORMATTER, {pattern: 'i'});
        v.getValue = function (index) {
          return index + 1;
        };
        return v;
      };

      track.showPopup = function (e, isHeader) {
        if (e.preventDefault) {
          e.preventDefault();
        }
      };
    }
  }
  ,
  resizeTrack: function (name, width, height, isColumns) {
    var index = this.getTrackIndex(name, isColumns);
    if (index === -1) {
      throw name + ' not found in resize track';
    }
    var heatMapPrefWidth = null;
    if (!isColumns) {
      var track = this.rowTracks[index];
      var header = this.rowTrackHeaders[index];
      track.setPrefWidth(width); // can only set width
      header.setPrefWidth(width);
    } else {
      var track = this.columnTracks[index];
      var header = this.columnTrackHeaders[index];
      if (height) {
        track.setPrefHeight(height);
        header.setPrefHeight(height);
      }
      if (width) {
        for (var i = 0; i < this.columnTracks.length; i++) {
          this.columnTracks[i].setPrefWidth(width);
          this.columnTrackHeaders[i].setPrefWidth(width);
        }
        // set width for all tracks
      }
    }
    this.revalidate();
    this.trigger('change', {
      name: 'resizeTrack',
      source: this,
      arguments: arguments
    });
  }
  ,
  isDendrogramVisible: function (isColumns) {
    var dendrogram = isColumns ? this.columnDendrogram : this.rowDendrogram;
    if (dendrogram !== undefined) {
      return phantasus.HeatMap.isDendrogramVisible(this.project, isColumns);
    }
  }
  ,
  /**
   *
   * Paint all the components
   *
   * @param options.paintRows
   * @param options.paintColumns
   * @param options.invalidateRows
   * @param options.invalidateColumns
   */
  paintAll: function (options) {
    var unscaledHeight = this.heatmap.getUnscaledHeight();
    var unscaledWidth = this.heatmap.getUnscaledWidth();
    var y = this.scrollTop();
    var x = this.scrollLeft();
    this.hscroll.paint();
    this.vscroll.paint(); // FIXME
    var rows = options.paintRows;
    var columns = options.paintColumns;
    var invalidateRows = options.invalidateRows;
    var invalidateColumns = options.invalidateColumns;
    // TODO double buffer search bars
    this.hSortByValuesIndicator.setInvalid(invalidateRows
      || invalidateColumns);
    this.hSortByValuesIndicator.paint({
      x: x,
      y: 0,
      width: unscaledWidth,
      height: this.hSortByValuesIndicator.getUnscaledHeight()
    });
    this.vSortByValuesIndicator.setInvalid(invalidateRows
      || invalidateColumns);
    this.vSortByValuesIndicator.paint({
      x: 0,
      y: y,
      width: this.vSortByValuesIndicator.getUnscaledWidth(),
      height: unscaledHeight
    });
    if (rows) {
      for (var i = 0, length = this.rowTracks.length; i < length; i++) {
        var track = this.rowTracks[i];
        track.setInvalid(invalidateRows);
        if (track.isVisible()) {
          track.paint({
            x: 0,
            y: y,
            height: unscaledHeight,
            width: unscaledWidth
          });
          this.rowTrackHeaders[i].paint();
        }
      }
      if (this.rowDendrogram != null) {
        this.rowDendrogram.setInvalid(invalidateRows);
        if (this.isDendrogramVisible(false)) {
          this.rowDendrogram.setVisible(true);
          this.rowDendrogram.paint({
            x: 0,
            y: y,
            height: unscaledHeight,
            width: this.rowDendrogram.getUnscaledWidth()
          });
        } else {
          this.rowDendrogram.setVisible(false);
        }
      }
    }
    if (columns) {
      for (var i = 0, length = this.columnTracks.length; i < length; i++) {
        var track = this.columnTracks[i];
        track.setInvalid(invalidateColumns);
        track.paint({
          x: x,
          y: 0,
          width: unscaledWidth,
          height: track.getUnscaledHeight()
        });
        this.columnTrackHeaders[i].paint();
      }
      if (this.columnDendrogram != null) {
        this.columnDendrogram.setInvalid(invalidateColumns);
        if (this.isDendrogramVisible(true)) {
          this.columnDendrogram.setVisible(true);
          this.columnDendrogram.paint({
            x: x,
            y: 0,
            width: unscaledWidth,
            height: this.columnDendrogram.getUnscaledHeight()
          });
        } else {
          this.columnDendrogram.setVisible(false);
        }
      }
    }
    if (invalidateRows || invalidateColumns) {
      this.heatmap.setInvalid(true);
    }
    this.heatmap.paint({
      x: x,
      y: y,
      width: unscaledWidth,
      height: unscaledHeight
    });
    this.trigger('change', {
      name: 'paintAll',
      source: this,
      arguments: arguments
    });
  }
  ,
  scrollTop: function (pos) {
    if (pos === undefined) {
      return this.vscroll.getValue();
    }
    if (isNaN(pos)) {
      pos = 0;
    }
    if (this.vscroll.getVisibleExtent() === this.vscroll.getTotalExtent()) {
      pos = 0;
    }
    pos = Math.max(pos, 0);
    pos = Math.min(this.vscroll.getMaxValue(), pos);
    if (pos !== this.vscroll.getValue()) {
      this.vscroll.setValue(pos, true);
      this.trigger('change', {
        name: 'scrollTop',
        source: this,
        arguments: arguments
      });
    }
    return pos;
  }
  ,
  scrollLeft: function (pos) {
    if (pos === undefined) {
      return this.hscroll.getValue();
    }
    if (isNaN(pos)) {
      pos = 0;
    }
    if (this.hscroll.getVisibleExtent() === this.hscroll.getTotalExtent()) {
      pos = 0;
    }
    pos = Math.max(pos, 0);
    pos = Math.min(this.hscroll.getMaxValue(), pos);
    if (pos !== this.hscroll.getValue()) {
      this.trigger('change', {
        name: 'scrollLeft',
        source: this,
        arguments: arguments
      });
      this.hscroll.setValue(pos, true);
    }
    return pos;
  }
  ,
  getSelectedTrackName: function (isColumns) {
    return isColumns ? this.selectedColumnTrackName : this.selectedRowTrackName;
  },
  getLastSelectedTrackInfo: function () {
    return this.selectedTrackInfo;
  },
  setSelectedTrack: function (name, isColumns) {
    var previousName = isColumns ? this.selectedColumnTrackName : this.selectedRowTrackName;
    if (name !== previousName) {
      var index = this.getTrackIndex(previousName, isColumns); // de-select previous
      if (index !== -1) {
        this.getTrackHeaderByIndex(index, isColumns).setSelected(false);
      }
      if (isColumns) {
        this.selectedColumnTrackName = name;
        this.selectedTrackInfo = {name: name, isColumns: true};
      } else {
        this.selectedRowTrackName = name;
        this.selectedTrackInfo = {name: name, isColumns: false};
      }

      var index = this.getTrackIndex(name, isColumns);
      if (index !== -1) {
        this.getTrackHeaderByIndex(index, isColumns).setSelected(true);
      }
      this.trigger('change', {
        name: 'setSelected',
        source: this,
        arguments: arguments
      });
    }
  }
  ,
  saveImage: function (file, format) {
    var _this = this;
    var bounds = this.getTotalSize();
    if (format === 'pdf') {
      var context = new canvas2pdf.PdfContext(blobStream(), {size: [bounds.width, bounds.height]});
      this.snapshot(context);
      context.stream.on('finish', function () {
        var blob = context.stream.toBlob('application/pdf');
        saveAs(blob, file, true);
      });
      context.end();
    } else if (format === 'svg') {
      var context = new C2S(bounds.width, bounds.height);
      this.snapshot(context);
      var svg = context.getSerializedSvg();
      var prefix = [];
      prefix.push('<?xml version="1.0" encoding="utf-8"?>\n');
      prefix.push('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"' +
        ' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n');
      svg = prefix.join('') + svg;
      var blob = new Blob([svg], {
        type: 'text/plain;charset=utf-8'
      });
      saveAs(blob, file, true);
    } else {
      var canvas = $('<canvas></canvas>')[0];
      var height = bounds.height;
      var width = bounds.width;
      var backingScale = phantasus.CanvasUtil.BACKING_SCALE;
      canvas.height = backingScale * height;
      canvas.style.height = height + 'px';
      canvas.width = backingScale * width;
      canvas.style.width = width + 'px';
      var context = canvas.getContext('2d');
      phantasus.CanvasUtil.resetTransform(context);
      this.snapshot(context);
      var toBlob = canvas.toBlobHD ? ['toBlobHD'] : 'toBlob';
      canvas[toBlob](function (blob) {
        if (blob == null || blob.size === 0) {
          phantasus.FormBuilder.showInModal({
            title: 'Save Image',
            html: 'Image is too large to save.',
            appendTo: _this.getContentEl(),
            focus: _this.getFocusEl()
          });
          return;
        }
        saveAs(blob, file, true);
      });
    }
  }
  ,
  getTotalSize: function (options) {
    options = $.extend({}, {
      legend: true
    }, options);
    var _this = this;
    var heatmapPrefSize = this.heatmap.getPreferredSize();
    var totalSize = {
      width: heatmapPrefSize.width,
      height: heatmapPrefSize.height
    };
    if (this.isDendrogramVisible(false)) { // row dendrogram
      totalSize.width += this.rowDendrogram.getUnscaledWidth() + phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS;
    }
    if (this.isDendrogramVisible(true)) {
      totalSize.height += this.columnDendrogram.getUnscaledHeight() + phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS;
    }
    var maxRowHeaderHeight = 0;
    for (var i = 0, length = this.rowTracks.length; i < length; i++) {
      var track = this.rowTracks[i];
      if (track.isVisible()) {
        var headerSize = this.rowTrackHeaders[i].getPrintSize();
        totalSize.width += Math.max(headerSize.width, track.getPrintSize().width);
        maxRowHeaderHeight = Math.max(maxRowHeaderHeight, headerSize.height);
      }
    }
    var maxColumnHeaderWidth = 0;
    var columnTrackHeightSum = 0;
    for (var i = 0, length = this.columnTracks.length; i < length; i++) {
      var track = this.columnTracks[i];
      if (track.isVisible()) {
        columnTrackHeightSum += track.getPrintSize().height;
        maxColumnHeaderWidth = Math.max(maxColumnHeaderWidth,
          this.columnTrackHeaders[i].getPrintSize().width);
      }
    }
    totalSize.height += Math.max(columnTrackHeightSum, maxRowHeaderHeight) + phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS;
    totalSize.width += maxColumnHeaderWidth + phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS;
    // color legend
    if (options.legend) {
      var totalLegendWidth = 15;
      var maxLegendHeight = 0;
      var colorByValues = this.heatmap.getColorScheme().getColorByValues();
      var ntracks = colorByValues.length;
      for (var i = 0; i < ntracks; i++) {
        var value = colorByValues[i];
        if (value != null || ntracks === 1) {
          // if (value != 'null') { // values are stored as string
          //
          // }

          this.heatmap.getColorScheme().setCurrentValue(value);
          var names = this.heatmap.getColorScheme().getNames();
          maxLegendHeight = Math.max(maxLegendHeight, names != null ? names.length * 14 : 30);
          totalLegendWidth += 250;
        }
      }
      maxLegendHeight += 10; // spacer
      totalSize.height = totalSize.height + maxLegendHeight;
      totalSize.width = Math.max(totalSize.width, totalLegendWidth);
    }

    // color
    var trackLegendSize = new phantasus.HeatMapTrackColorLegend(
      _.filter(
        this.columnTracks,
        function (track) {
          return track.isVisible()
            && (track.isRenderAs(phantasus.VectorTrack.RENDER.COLOR) || track.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_COLOR));
        }), this.getProject().getColumnColorModel()).getPreferredSize();
    totalSize.height += trackLegendSize.height;
    totalSize.width = Math.max(totalSize.width, trackLegendSize.width);

    trackLegendSize = new phantasus.HeatMapTrackColorLegend(
      _.filter(
        this.rowTracks,
        function (track) {
          return track.isVisible()
            && (track.isRenderAs(phantasus.VectorTrack.RENDER.COLOR) || track.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_COLOR));
        }), this.getProject().getRowColorModel()).getPreferredSize();
    totalSize.height += trackLegendSize.height;
    totalSize.width = Math.max(totalSize.width, trackLegendSize.width);

    // shape
    trackLegendSize = new phantasus.HeatMapTrackShapeLegend(
      _.filter(
        this.columnTracks,
        function (track) {
          return track.isVisible()
            && (track.isRenderAs(phantasus.VectorTrack.RENDER.SHAPE));
        }), this.getProject().getColumnShapeModel()).getPreferredSize();
    totalSize.height += trackLegendSize.height;
    totalSize.width = Math.max(totalSize.width, trackLegendSize.width);

    trackLegendSize = new phantasus.HeatMapTrackShapeLegend(
      _.filter(
        this.rowTracks,
        function (track) {
          return track.isVisible()
            && (track.isRenderAs(phantasus.VectorTrack.RENDER.SHAPE));
        }), this.getProject().getRowShapeModel()).getPreferredSize();
    totalSize.height += trackLegendSize.height;
    totalSize.width = Math.max(totalSize.width, trackLegendSize.width);

    // font
    trackLegendSize = new phantasus.HeatMapTrackShapeLegend(
      _.filter(
        this.columnTracks,
        function (track) {
          return track.isVisible()
            && (track.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_FONT));
        }), this.getProject().getColumnFontModel()).getPreferredSize();
    totalSize.height += trackLegendSize.height;
    totalSize.width = Math.max(totalSize.width, trackLegendSize.width);

    trackLegendSize = new phantasus.HeatMapTrackShapeLegend(
      _.filter(
        this.rowTracks,
        function (track) {
          return track.isVisible()
            && (track.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_FONT));
        }), this.getProject().getRowFontModel()).getPreferredSize();
    totalSize.height += trackLegendSize.height;
    totalSize.width = Math.max(totalSize.width, trackLegendSize.width);
    return totalSize;
  }
  ,
  getHeatMapElementComponent: function () {
    return this.heatmap;
  }
  ,
  snapshot: function (context, options) {
    options = $.extend({}, {
      legend: true
    }, options);
    var heatmapPrefSize = this.heatmap.getPreferredSize();
    var totalSize = this.getTotalSize(options);
    var legendHeight = 0;
    if (options.legend) {
      var colorByValues = this.heatmap.getColorScheme().getColorByValues();
      context.save();
      context.translate(15, 0);
      var ntracks = colorByValues.length;
      for (var i = 0, ntracks = colorByValues.length; i < ntracks; i++) {
        var value = colorByValues[i];
        if (value != null || ntracks === 1) {
          if (value != 'null') { // values are stored as string
            // var $label = $('<div style="overflow:hidden;text-overflow:' +
            //   ' ellipsis;width:250px;max-width:250px;">'
            //   + value + '</div>');
            // $keyContent.append($label);
            // totalHeight += $label.height();
          }
          var trackLegend = new phantasus.ColorSupplierLegend(
            this.heatmap.getColorScheme(), value);
          trackLegend.draw({}, context);
          legendHeight = Math.max(legendHeight, trackLegend.getUnscaledHeight());
          var legendWidth = trackLegend.getUnscaledWidth();
          context.translate(legendWidth, 0);
        }
      }
      legendHeight += 10; // spacer

      // phantasus.HeatMapColorSchemeLegend.drawColorScheme(context,
      //   this.heatmap.getColorScheme(), 200, true);
      context.restore();
      // legendHeight = this.heatmap.getColorScheme().getNames() != null ? this.heatmap
      //   .getColorScheme().getNames().length * 14
      //   : 40;
    }
    var legendOffset = 15;
    var maxLegendHeight = 0;

    // color legend
    context.save();
    context.translate(legendOffset, legendHeight);
    var trackLegend = new phantasus.HeatMapTrackColorLegend(
      _.filter(
        this.columnTracks,
        function (track) {
          return track.isVisible()
            && (track.isRenderAs(phantasus.VectorTrack.RENDER.COLOR) || track.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_COLOR));
        }), this.getProject().getColumnColorModel());
    trackLegend.draw({}, context);
    var legendSize = trackLegend.getPreferredSize();
    legendOffset += legendSize.width;
    maxLegendHeight = Math.max(maxLegendHeight, legendSize.height);
    context.restore();

    // shape legend
    context.save();
    context.translate(legendOffset, legendHeight);
    trackLegend = new phantasus.HeatMapTrackShapeLegend(
      _.filter(
        this.columnTracks,
        function (track) {
          return track.isVisible()
            && (track.isRenderAs(phantasus.VectorTrack.RENDER.SHAPE));
        }), this.getProject().getColumnShapeModel());
    trackLegend.draw({}, context);
    legendSize = trackLegend.getPreferredSize();
    legendOffset += legendSize.width;
    maxLegendHeight = Math.max(maxLegendHeight, legendSize.height);
    context.restore();

    // font legend
    context.save();
    context.translate(legendOffset, legendHeight);
    trackLegend = new phantasus.HeatMapTrackFontLegend(
      _.filter(
        this.columnTracks,
        function (track) {
          return track.isVisible()
            && (track.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_FONT));
        }), this.getProject().getColumnFontModel());
    trackLegend.draw({}, context);
    legendSize = trackLegend.getPreferredSize();
    legendOffset += legendSize.width;
    maxLegendHeight = Math.max(maxLegendHeight, legendSize.height);
    context.restore();

    // row color legend
    context.save();
    context.translate(legendOffset, legendHeight);
    trackLegend = new phantasus.HeatMapTrackColorLegend(
      _.filter(
        this.rowTracks,
        function (track) {
          return track.isVisible()
            && (track.isRenderAs(phantasus.VectorTrack.RENDER.COLOR) || track.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_COLOR));
        }), this.getProject().getRowColorModel());
    trackLegend.draw({}, context);
    legendSize = trackLegend.getPreferredSize();
    legendOffset += legendSize.width;
    maxLegendHeight = Math.max(maxLegendHeight, legendSize.height);
    context.restore();

    // shape legend
    context.save();
    context.translate(legendOffset, legendHeight);
    trackLegend = new phantasus.HeatMapTrackShapeLegend(
      _.filter(
        this.rowTracks,
        function (track) {
          return track.isVisible()
            && (track.isRenderAs(phantasus.VectorTrack.RENDER.SHAPE));
        }), this.getProject().getRowShapeModel());
    trackLegend.draw({}, context);
    legendSize = trackLegend.getPreferredSize();
    legendOffset += legendSize.width;
    maxLegendHeight = Math.max(maxLegendHeight, legendSize.height);
    context.restore();

    // font legend
    context.save();
    context.translate(legendOffset, legendHeight);
    trackLegend = new phantasus.HeatMapTrackFontLegend(
      _.filter(
        this.rowTracks,
        function (track) {
          return track.isVisible()
            && (track.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_FONT));
        }), this.getProject().getRowFontModel());
    trackLegend.draw({}, context);
    legendSize = trackLegend.getPreferredSize();
    legendOffset += legendSize.width;
    maxLegendHeight = Math.max(maxLegendHeight, legendSize.height);
    context.restore();

    legendHeight += maxLegendHeight;

    var heatmapY = this.isDendrogramVisible(true) ? (this.columnDendrogram.getUnscaledHeight() +
      phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS) : 0;
    heatmapY += legendHeight;
    var columnTrackY = heatmapY;
    var heatmapX = this.isDendrogramVisible(false) ? (this.rowDendrogram.getUnscaledWidth() +
      phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS) : 0;
    var isColumnTrackVisible = false;
    for (var i = 0, length = this.columnTracks.length; i < length; i++) {
      var track = this.columnTracks[i];
      if (track.isVisible()) {
        var header = this.columnTrackHeaders[i];
        heatmapX = Math.max(heatmapX, header.getPrintSize().width);
        heatmapY += track.getPrintSize().height;
        isColumnTrackVisible = true;
      }
    }
    if (isColumnTrackVisible) {
      heatmapY += phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS;
    }

    // check if row headers are taller than column tracks
    for (var i = 0, length = this.rowTracks.length; i < length; i++) {
      var track = this.rowTracks[i];
      if (track.isVisible()) {
        var header = this.rowTrackHeaders[i];
        heatmapY = Math.max(heatmapY, header.getPrintSize().height);
      }
    }
    if (this.isDendrogramVisible(true)) {
      var columnDendrogramClip = {
        x: 0,
        y: 0,
        height: this.columnDendrogram.getUnscaledHeight(),
        width: heatmapPrefSize.width
      };
      context.save();
      context.translate(heatmapX, legendHeight);
      this.columnDendrogram.prePaint(columnDendrogramClip, context);
      this.columnDendrogram.draw(columnDendrogramClip, context);
      context.restore();
    }
    if (this.isDendrogramVisible(false)) {
      var rowDendrogramClip = {
        x: 0,
        y: 0,
        width: this.rowDendrogram.getUnscaledWidth(),
        height: heatmapPrefSize.height
      };
      context.save();
      context.translate(0, heatmapY);
      this.rowDendrogram.prePaint(rowDendrogramClip, context);
      this.rowDendrogram.draw(rowDendrogramClip, context);
      context.restore();
    }

    for (var i = 0, length = this.columnTracks.length; i < length; i++) {
      var track = this.columnTracks[i];
      if (track.isVisible()) {
        context.save();
        context.translate(heatmapX, columnTrackY);
        var trackClip = {
          x: 0,
          y: 0,
          width: heatmapPrefSize.width,
          height: track.getPrintSize().height
        };
        track.print(trackClip, context);
        context.restore();
        // draw header
        var header = this.columnTrackHeaders[i];
        context.save();
        var headerSize = header.getPrintSize();
        var headerClip = {
          x: 0,
          y: 0,
          width: headerSize.width,
          height: trackClip.height
        };
        context.translate(heatmapX - 2, columnTrackY + trackClip.height);
        header.print(headerClip, context);
        context.restore();
        columnTrackY += Math.max(headerClip.height, trackClip.height);
      }
    }
    context.save();
    context.translate(heatmapX, heatmapY);

    this.heatmap.draw({
      x: 0,
      y: 0,
      width: heatmapPrefSize.width,
      height: heatmapPrefSize.height
    }, context);
    context.restore();
    var rowTrackWidthSum = 0;
    for (var i = 0, length = this.rowTracks.length; i < length; i++) {
      var track = this.rowTracks[i];
      if (track.isVisible()) {
        context.save();
        var tx = phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS + heatmapX + heatmapPrefSize.width + rowTrackWidthSum;
        var ty = heatmapY;
        var trackClip = {
          x: 0,
          y: 0,
          width: track.getPrintSize().width,
          height: heatmapPrefSize.height
        };
        context.translate(tx, ty);
        context.strokeStyle = 'white';
        context.rect(0, 0, trackClip.width, trackClip.height);
        // stroke is needed for clip to work for svg export
        context.stroke();
        context.clip();

        track.print(trackClip, context);
        context.restore();
        // draw header
        var header = this.rowTrackHeaders[i];
        context.save();
        var headerSize = header.getPrintSize();
        var headerClip = {
          x: 0,
          y: 0,
          width: headerSize.width,
          height: headerSize.height
        };
        context.translate(tx, ty - 4);
        header.print(headerClip, context);
        context.restore();
        rowTrackWidthSum += Math.max(headerSize.width, trackClip.width);
      }
    }
  }
  ,
  resetZoom: function () {
    var heatmap = this.heatmap;
    var rowSizes = heatmap.getRowPositions();
    var columnSizes = heatmap.getColumnPositions();
    rowSizes.setSize(13);
    columnSizes.setSize(13);
    var reval = {};
    if (this.project.getHoverRowIndex() !== -1) {
      reval.scrollTop = this.heatmap.getRowPositions().getPosition(
        this.project.getHoverRowIndex());
    }
    if (this.project.getHoverColumnIndex() !== -1) {
      reval.scrollLeft = this.heatmap.getColumnPositions().getPosition(
        this.project.getHoverColumnIndex());
    }
    this.revalidate(reval);
  }
  ,
  getFitColumnSize: function () {
    var heatmap = this.heatmap;
    var availablePixels = this.getAvailableWidth();
    if (availablePixels === -1) {
      return 13;
    }
    if (this.rowDendrogram) {
      availablePixels -= this.rowDendrogram.getUnscaledWidth();
    }
    var trackPixels = 12; // spacer
    for (var i = 0, length = this.rowTracks.length; i < length; i++) {
      var track = this.rowTracks[i];
      if (track.isVisible()) {
        trackPixels += track.getUnscaledWidth();
      }
    }
    for (var i = 0, length = this.columnTracks.length; i < length; i++) {
      var track = this.columnTracks[i];
      if (track.isVisible()) { // all column track headers have the
        // same width
        trackPixels += this.columnTrackHeaders[i].getUnscaledWidth();
        break;
      }
    }

    availablePixels -= trackPixels;

    var positions = heatmap.getColumnPositions();
    var totalCurrent = positions.getItemSize(positions.getLength() - 1)
      + positions.getPosition(positions.getLength() - 1);
    var size = positions.getSize();
    size = size * (availablePixels / totalCurrent);
    size = Math.min(13, size);
    return size;
  }
  ,
  getFitRowSize: function () {
    var heatmap = this.heatmap;
    var availablePixels = this.getAvailableHeight();
    if (availablePixels === -1) {
      return 13;
    }
    if (this.columnDendrogram) {
      availablePixels -= this.columnDendrogram.getUnscaledHeight();
    }
    var trackPixels = 12;
    for (var i = 0, length = this.columnTracks.length; i < length; i++) {
      var track = this.columnTracks[i];
      if (track.isVisible()) {
        trackPixels += track.getUnscaledHeight();
      }
    }
    availablePixels -= trackPixels;
    var positions = heatmap.getRowPositions();
    var totalCurrent = positions.getItemSize(positions.getLength() - 1)
      + positions.getPosition(positions.getLength() - 1);

    var size = positions.getSize();
    size = size * (availablePixels / totalCurrent);
    size = Math.min(13, size);
    return size;
  }
  ,
  /**
   * @param options.fitRows
   * @param options.fitColumns
   * @param options.repaint
   */
  fitToWindow: function (options) {
    if (options.fitRows) {
      this.heatmap.getRowPositions().setSize(this.getFitRowSize());
    }
    if (options.fitColumns) {
      this.heatmap.getColumnPositions().setSize(this.getFitColumnSize());
    }
    if (options.repaint) {
      var revalOptions = {};
      if (options.fitRows) {
        if (this.project.getHoverRowIndex() !== -1) {
          revalOptions.scrollTop = this.heatmap.getRowPositions().getPosition(
            this.project.getHoverRowIndex());
        }
      }
      if (options.fitColumns) {
        if (this.project.getHoverColumnIndex() !== -1) {
          revalOptions.scrollLeft = this.heatmap.getColumnPositions().getPosition(this.project.getHoverColumnIndex());
        }
      }
      this.revalidate(revalOptions);
    }
  }
  ,
  getAvailableHeight: function () {
    if (_.isNumber(this.options.height)) {
      return this.options.height;
    }
    var height = $(window).height() - this.$parent.offset().top - 24;
    if (this.options.height === 'window') {
      return height;
    }
    return Math.max(Math.round(screen.height * 0.7), height);
  }
  ,
  getAvailableWidth: function () {
    if (this.options.width) {
      return this.options.width;
    }
    // (this.$el.parent().outerWidth() - 30);
    // return this.$el.width() - 30;

    return this.tabManager.getWidth() - 30;
  }
  ,
  /**
   * Layout all the components
   */
  revalidate: function (options) {
    if (phantasus.Util.isHeadless()) {
      // hack to force creation of color scheme
      for (var i = 0, length = this.rowTracks.length; i < length; i++) {
        var track = this.rowTracks[i];
        track.setInvalid(true);
        if (track.isVisible()) {
          track.paint({
            x: 0,
            y: 0,
            height: 10,
            width: 10
          });
        }
      }
      for (var i = 0, length = this.columnTracks.length; i < length; i++) {
        var track = this.columnTracks[i];
        track.setInvalid(true);
        if (track.isVisible()) {
          track.paint({
            x: 0,
            y: 0,
            height: 10,
            width: 10
          });
        }
      }
      return;
    }
    options = $.extend({}, {
      paint: true
    }, options);
    this.updatingScroll = true;
    var availableHeight = this.getAvailableHeight();
    var availableWidth = this.getAvailableWidth();
    var heatmapPrefSize = this.heatmap.getPreferredSize();

    var columnDendrogramHeight = 0;
    var rowDendrogramWidth = 0;
    if (this.columnDendrogram) {
      columnDendrogramHeight = phantasus.CanvasUtil.getPreferredSize(this.columnDendrogram).height;
    }
    if (this.rowDendrogram) {
      rowDendrogramWidth = phantasus.CanvasUtil.getPreferredSize(this.rowDendrogram).width;
    }
    var rowTrackWidthSum = 0;
    for (var i = 0, length = this.rowTracks.length; i < length; i++) {
      if (this.rowTracks[i].isVisible()) {
        // was manually resized
        if (this.rowTracks[i].getPrefWidth() !== undefined) {
          this.rowTrackHeaders[i].setPrefWidth(this.rowTracks[i].getPrefWidth());
        }
        rowTrackWidthSum += Math.max(phantasus.CanvasUtil.getPreferredSize(this.rowTrackHeaders[i]).width,
          phantasus.CanvasUtil.getPreferredSize(this.rowTracks[i]).width);
      }
    }
    if (availableWidth !== -1 && (rowTrackWidthSum + rowDendrogramWidth + heatmapPrefSize.width) > availableWidth) {
      // shrink row tracks
      //var over = (rowTrackWidthSum + rowDendrogramWidth + heatmapPrefSize.width) - availableWidth;
      rowTrackWidthSum = 0;
      for (var i = 0, length = this.rowTracks.length; i < length; i++) {
        if (this.rowTracks[i].isVisible()) {
          var rowTrackHeaderSize = phantasus.CanvasUtil.getPreferredSize(this.rowTrackHeaders[i]);
          var width = Math.max(rowTrackHeaderSize.width, phantasus.CanvasUtil.getPreferredSize(this.rowTracks[i]).width);
          if (!rowTrackHeaderSize.widthSet) {
            width = Math.min(400, width);
            this.rowTracks[i].setPrefWidth(width);
            this.rowTrackHeaders[i].setPrefWidth(width);
          }
          rowTrackWidthSum += width;
        }
      }
    }

    var ypos = columnDendrogramHeight;
    var maxHeaderWidth = 0;
    // get max column header width
    for (var i = 0, length = this.columnTracks.length; i < length; i++) {
      if (this.columnTracks[i].isVisible()) {
        if (this.columnTracks[i].getPrefHeight() !== undefined) {
          this.columnTrackHeaders[i].setPrefHeight(this.columnTracks[i].getPrefHeight());
        }
        var width = phantasus.CanvasUtil.getPreferredSize(this.columnTrackHeaders[i]).width;
        maxHeaderWidth = Math.max(maxHeaderWidth, width);
      }
    }
    var xpos = Math.max(rowDendrogramWidth, maxHeaderWidth);
    var heatMapWidth = heatmapPrefSize.width;
    var maxHeatMapWidth = Math.max(50, availableWidth === -1 ? Number.MAX_VALUE : (availableWidth - rowTrackWidthSum
    - xpos
    - phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS));
    if (maxHeatMapWidth > 0 && heatMapWidth > maxHeatMapWidth) {
      heatMapWidth = maxHeatMapWidth;
      heatMapWidth = Math.min(heatMapWidth, heatmapPrefSize.width); // can't
      // go
      // bigger
      // than
      // pref
      // width
    }
    if (this.heatmap.prefWidth !== undefined) { // heat map was manually
      // resized
      heatMapWidth = Math.min(heatmapPrefSize.width,
        this.heatmap.prefWidth);
    }
    if (this.columnDendrogram !== undefined) {
      this.columnDendrogram.setBounds({
        width: heatMapWidth,
        height: columnDendrogramHeight,
        left: xpos,
        top: 0
      });
      this.columnDendrogram.$label.css('left',
        xpos + this.columnDendrogram.getUnscaledWidth() + 10).css(
        'top', 2);
      this.columnDendrogram.$squishedLabel.css('left',
        xpos + this.columnDendrogram.getUnscaledWidth() + 10).css(
        'top', 18);

      this.beforeColumnTrackDivider.setVisible(true);
      this.beforeColumnTrackDivider.setBounds({
        left: xpos - maxHeaderWidth,
        top: ypos,
        width: maxHeaderWidth
      });
      ypos++;
    } else {
      this.beforeColumnTrackDivider.setVisible(false);
    }

    for (var i = 0, length = this.columnTracks.length; i < length; i++) {
      var track = this.columnTracks[i];
      if (track.isVisible()) {
        var size = phantasus.CanvasUtil.getPreferredSize(track);
        var headerSize = phantasus.CanvasUtil.getPreferredSize(this.columnTrackHeaders[i]);
        size.height = Math.max(size.height, headerSize.height);
        track.setBounds({
          width: heatMapWidth,
          height: size.height,
          left: xpos,
          top: ypos
        });
        this.columnTrackHeaders[i].setBounds({
          width: maxHeaderWidth,
          height: size.height,
          left: xpos - maxHeaderWidth,
          top: ypos
        });
        ypos += size.height;
      }
    }
    this.$whitespace[0].style.left = Math.ceil(xpos + heatMapWidth + 10) + 'px';
    this.$whitespace[0].style.top = '0px';
    ypos += phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS;
    var heatMapHeight = heatmapPrefSize.height;
    if (availableHeight !== -1 && heatMapHeight > (availableHeight - ypos)) {
      heatMapHeight = Math.max(100, Math.min(heatmapPrefSize.height,
        availableHeight - ypos));
    }
    if (ypos < 0) {
      ypos = 0;
    }
    if (this.rowDendrogram) {
      this.rowDendrogram.setBounds({
        width: Math.max(rowDendrogramWidth, maxHeaderWidth),
        height: heatMapHeight,
        left: 0,
        top: ypos
      });
      this.rowDendrogram.$label.css('left', 0).css('top', 2);
      this.afterRowDendrogramDivider.setVisible(true);
      this.afterRowDendrogramDivider.setBounds({
        height: heatMapHeight,
        left: this.rowDendrogram.getUnscaledWidth(),
        top: ypos
      });
      xpos++;
    } else {
      this.afterRowDendrogramDivider.setVisible(false);
    }
    this.heatmap.setBounds({
      width: heatMapWidth,
      height: heatMapHeight,
      left: xpos,
      top: ypos
    });
    this.hSortByValuesIndicator.setBounds({
      height: 4,
      width: heatMapWidth,
      left: xpos,
      top: ypos - 4
    });
    this.hscroll.setVisible(heatMapWidth < heatmapPrefSize.width);
    this.hscroll.setExtent(heatMapWidth, heatmapPrefSize.width,
      options.scrollLeft !== undefined ? options.scrollLeft
        : (heatmapPrefSize.width === this.hscroll.getTotalExtent() ? this.hscroll.getValue()
        : heatmapPrefSize.width
      * this.hscroll.getValue()
      / this.hscroll.getMaxValue()));
    this.hscroll.setBounds({
      left: xpos,
      top: ypos + heatMapHeight + 2
    });
    xpos += heatMapWidth;
    var nvisibleRowTracks = 0;
    for (var i = 0, length = this.rowTracks.length; i < length; i++) {
      var track = this.rowTracks[i];
      if (track.isVisible()) {
        nvisibleRowTracks++;
        break;
      }
    }
    this.vSortByValuesIndicator.setBounds({
      width: 4,
      height: heatMapHeight,
      left: xpos,
      top: ypos
    });
    if (nvisibleRowTracks > 0) {
      xpos = xpos
        + phantasus.HeatMap.SPACE_BETWEEN_HEAT_MAP_AND_ANNOTATIONS; // leave
      // space
      // after
      // afterVerticalScrollBarDivider
    }
    var rowAnnotationXStart = xpos;
    // set row track bounds
    for (var i = 0, length = this.rowTracks.length; i < length; i++) {
      var track = this.rowTracks[i];
      if (track.isVisible()) {
        var size = phantasus.CanvasUtil.getPreferredSize(track);
        var headerSize = phantasus.CanvasUtil.getPreferredSize(this.rowTrackHeaders[i]);
        size.width = Math.max(headerSize.width, size.width);
        size.height = heatMapHeight;
        track.setBounds({
          width: size.width,
          height: size.height,
          left: xpos,
          top: ypos
        });

        this.rowTrackHeaders[i].setBounds({
          width: size.width,
          left: xpos,
          top: ypos - headerSize.height - 5,
          height: headerSize.height
        });
        xpos += size.width;
      }
    }
    this.afterVerticalScrollBarDivider.setVisible(nvisibleRowTracks > 0 ? true : false);
    this.afterVerticalScrollBarDivider.setBounds({
      left: rowAnnotationXStart - 2,
      top: ypos - 18
    });
    this.vscroll.setVisible(heatMapHeight < heatmapPrefSize.height);
    this.vscroll.setExtent(heatMapHeight, heatmapPrefSize.height,
      options.scrollTop !== undefined ? options.scrollTop
        : (heatmapPrefSize.height === this.vscroll.getTotalExtent() ? this.vscroll.getValue()
        : heatmapPrefSize.height
      * this.vscroll.getValue()
      / this.vscroll.getMaxValue()));
    xpos += 2;
    this.vscroll.setBounds({
      left: xpos,
      top: ypos
    });
    xpos += this.vscroll.getUnscaledWidth();
    if (this.hscroll.isVisible()) {
      ypos += this.hscroll.getUnscaledHeight() + 2;
    }
    var totalHeight = 2 + ypos + heatMapHeight;
    if (options.paint) {
      this.paintAll({
        paintRows: true,
        paintColumns: true,
        invalidateRows: true,
        invalidateColumns: true
      });
    }
    this.$parent.css({
      height: Math.ceil(totalHeight) + 'px',
      width: availableWidth === -1 ? (Math.ceil(xpos + 2) + 'px') : ''
    });

    this.updatingScroll = false;
    this.trigger('change', {
      name: 'revalidate',
      source: this,
      arguments: arguments
    });
  }
};
phantasus.HeatMap.copyFromParent = function (project, options) {
  // TODO persist sort order, grouping, dendrogram

  project.rowColorModel = options.parent.getProject().getRowColorModel().copy();
  project.columnColorModel = options.parent.getProject().getColumnColorModel().copy();

  project.rowShapeModel = options.parent.getProject().getRowShapeModel().copy();
  project.columnShapeModel = options.parent.getProject().getColumnShapeModel().copy();

  project.rowFontModel = options.parent.getProject().getRowFontModel().copy();
  project.columnFontModel = options.parent.getProject().getColumnFontModel().copy();

  var parentRowTracks = options.parent.rowTracks || [];
  var parentColumnTracks = options.parent.columnTracks || [];
  if (options.inheritFromParentOptions.rows) { // row similarity matrix
    project.columnShapeModel = project.rowShapeModel;
    project.columnColorModel = project.rowColorModel;
    project.columnFontModel = project.rowFontModel;
    parentColumnTracks = parentRowTracks.slice().reverse();
  }
  if (options.inheritFromParentOptions.columns) { // column similarity matrix
    project.rowShapeModel = project.columnShapeModel;
    project.rowColorModel = project.columnColorModel;
    project.rowFontModel = project.columnFontModel;
    parentRowTracks = parentColumnTracks.slice().reverse();
  }

  if (options.inheritFromParentOptions.transpose) {
    var tmp = project.rowShapeModel;
    project.rowShapeModel = project.columnShapeModel;
    project.columnShapeModel = tmp;

    tmp = project.rowColorModel;
    project.rowColorModel = project.columnColorModel;
    project.columnColorModel = tmp;

    tmp = project.rowFontModel;
    project.rowFontModel = project.columnFontModel;
    project.columnFontModel = tmp;

    tmp = parentRowTracks.slice().reverse();
    // swap tracks
    parentRowTracks = parentColumnTracks.slice().reverse();
    parentColumnTracks = tmp;
  }
  // copy track rendering options and order
  // from parent
  options.rows = options.rows || [];

  for (var i = 0; i < parentRowTracks.length; i++) {
    var track = parentRowTracks[i];
    if (track.isVisible()) {
      options.rows.push({
        order: options.rows.length,
        field: track.getName(),
        display: $.extend(true, {}, track.settings),
        force: true
      });
    }
  }
  options.columns = options.columns || [];
  for (var i = 0; i < parentColumnTracks.length; i++) {
    var track = parentColumnTracks[i];
    if (track.isVisible()) {
      options.columns.push({
        order: options.columns.length,
        field: track.getName(),
        display: $.extend(true, {}, track.settings),
        force: true
      });
    }
  }
};
phantasus.Util.extend(phantasus.HeatMap, phantasus.Events);

phantasus.HelpMenu = function () {
  var html = [];
  html.push('<div class="btn-group">');
  html.push('<button type="button" class="btn btn-default btn-xxs' +
    ' dropdown-toggle"' +
    ' data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">');
  html.push('<svg width="14px" height="14px" style="inline-block;vertical-align:middle;"><g><rect x="0" y="0" width="14" height="6" style="fill:#ca0020;stroke:none"></rect><rect x="0" y="7" width="14" height="6" style="fill:#0571b0;stroke:none"></rect></g></svg>');
  html.push('<span class="fa fa-caret-down"></span>');
  html.push('</button>');
  html
    .push('<ul class="dropdown-menu dropdown-menu-right" role="menu">');
  html.push('<li><a data-name="contact" href="#">Contact</a></li>');

  html.push('<li><a data-name="tutorial" href="#">Tutorial</a></li>');
  html.push('<li><a data-name="source" href="#">Source Code</a></li>');

  html.push('</ul>');
  html.push('</div>');
  this.$el = $(html.join(''));
  this.$el.find('[data-name=contact]').on('click', function (e) {
    phantasus.FormBuilder.showInModal({
      title: 'Contact',
      html: 'Please email us at alsergbox@gmail.com',
      focus: document.activeElement
    });
    e.preventDefault();
  });
  this.$el.find('[data-name=tutorial]').on('click', function (e) {
    window
    .open('phantasus-tutorial.html');
    e.preventDefault();

  });

  this.$el.find('[data-name=source]').on('click', function (e) {
    window.open('https://github.com/ctlab/phantasus');
    e.preventDefault();

  });

  this.$el.find('[data-name=about]').on('click', function (e) {
    window.open('about.html');
    e.preventDefault();

  });
};

phantasus.HistogramLegend = function (dataset, colorScheme, metadataValue) {
  phantasus.AbstractCanvas.call(this, true);
  this.colorScheme = colorScheme;
  this.metadataValue = metadataValue;
  this.dataset = dataset;
  this.binNumberToOccurences = null;
  this.setBounds({
    width: 250,
    height: 70
  });
  this.name = null;
  this.canvas.style.position = '';
  this.canvas.style.border = '1px solid LightGrey';
};

phantasus.HistogramLegend.prototype = {
  binSize: 0,
  maxCount: 0,
  total: 0,
  setName: function (name) {
    this.name = name;
  },
  setBinSize: function (binSize) {
    this.binSize = binSize;
  },
  buildHistogram: function () {
    var binSize = this.binSize;
    var dataset = this.dataset;
    var metadataValue = this.metadataValue;
    var colorScheme = this.colorScheme;
    var min = colorScheme.getMin();
    var max = colorScheme.getMax();
    if (min === max) {
      min -= 0.5;
      max += 0.5;
    }
    var vector =
      dataset.getRowMetadata().getByName(colorScheme.getSeparateColorSchemeForRowMetadataField());
    // var numberOfBins = Math.ceil(phantasus.Log2(dataset.getRowCount() * dataset.getColumnCount()) + 1);
    // var binSize = (max - min) / numberOfBins;
    var numberOfBins = Math.ceil((max - min) / binSize);
    var binNumberToOccurences = new Uint32Array(numberOfBins);
    this.binNumberToOccurences = binNumberToOccurences;
    //var values = new Float32Array(dataset.getRowCount() * dataset.getColumnCount()); // for
    // boxplot
    var index = 0;
    for (var i = 0, nrows = dataset.getRowCount(); i < nrows; i++) {
      if (vector == null || vector.getValue(i) === metadataValue) {
        for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
          var value = dataset.getValue(i, j);
          if (isNaN(value)) {
            continue;
          }
          //    values[index++] = value;
          var bin = Math.floor(((value - min) / binSize));
          if (bin < 0) {
            bin = 0;
          } else if (bin >= numberOfBins) {
            bin = numberOfBins - 1;
          }
          binNumberToOccurences[bin]++;
        }
      }
    }
    // values = values.slice(0, index);
    // values.sort();

    var maxCount = 0;
    var total = 0;
    for (var i = 0; i < numberOfBins; i++) {
      var count = binNumberToOccurences[i];
      maxCount = count >= maxCount ? count : maxCount;
      total += count;
    }
    this.maxCount = maxCount;
    this.total = total;
  },
  draw: function (clip, context) {
    this.buildHistogram();
    var colorScheme = this.colorScheme;
    var canvasWidth = this.getUnscaledWidth() - 50;
    var valueToPosition = d3.scale.linear().domain([colorScheme.getMin(), colorScheme.getMax()]).range([0, canvasWidth]).clamp(
      true);
    var histogramHeight = 30;
    var countToPosition = d3.scale.linear().domain([0, this.maxCount / this.total]).range([histogramHeight, 0]).clamp(
      true);
    var binNumberToOccurences = this.binNumberToOccurences;
    var min = colorScheme.getMin();
    var binSize = this.binSize;
    var y0 = countToPosition(0);

    if (this.name != null) {
      context.font = '11px ' + phantasus.CanvasUtil.getFontFamily(context);
      context.fillStyle = 'black';
      context.lineWidth = 1;
      //  context.textBaseline = 'top';
      context.fillText(this.name, 0.5, 12);
      context.translate(0, 14);
    }
    context.lineWidth = 0.2;
    context.strokeStyle = '#D3D2C2';
    context.fillStyle = '#D3D2C2'; //'#d9d9d9';
    context.translate(25, 0);
    context.beginPath();
    context.moveTo(0, y0);
    context.lineTo(canvasWidth, y0);
    context.stroke();
    context.lineWidth = 1;
    context.strokeStyle = 'white';
    for (var i = 0, numberOfBins = binNumberToOccurences.length; i < numberOfBins; i++) {
      var count = binNumberToOccurences[i];
      if (count > 0) {
        count /= this.total;
        var start = min + (i * binSize);
        var end = start + binSize;
        var x = valueToPosition(start);
        var width = valueToPosition(end) - x;
        var y = countToPosition(count);
        context.rect(x, y0, width, y - y0);
        context.fill();
        context.stroke();
      }
    }
    // boxplot
    // var q25 = valueToPosition(this.q25);
    // var q75 = valueToPosition(this.q75);
    // var median = valueToPosition(this.median);
    // var lav = valueToPosition(this.lowerAdjacentValue);
    // var uav = valueToPosition(this.upperAdjacentValue);
    // context.translate(0, histogramHeight + 1);
    // context.fillStyle = 'black';
    //  var boxPlotHeight = 8;
    // context.fillRect(q25, 0, q75 - q25, boxPlotHeight);
    //
    // context.fillRect(lav, boxPlotHeight / 2 - 1, q25 - lav, 2);
    //
    // context.fillRect(q75, boxPlotHeight / 2 - 1, uav - q75, 2);
    //
    // context.fillStyle = 'white';
    // context.fillRect(median - 1, 0.5, 2, boxPlotHeight - 0.5);
    //
    context.translate(0, histogramHeight + 1);
    context.fillStyle = 'black';
    phantasus.HeatMapColorSchemeLegend.drawColorScheme(context,
      this.colorScheme, canvasWidth, false, false, 6);
  }
}
;

phantasus.Util.extend(phantasus.HistogramLegend, phantasus.AbstractCanvas);

phantasus.LegendWithStops = function () {
  var _this = this;
  phantasus.AbstractCanvas.call(this, false);
  this.setBounds({
    width: 300,
    height: 40
  });
  $(this.canvas).on('mousedown', function (event) {
    var position = phantasus.CanvasUtil.getMousePos(
      event.target, event);
    _this.selectedIndex = _this
    .findIndexForPosition(position);
    _this.trigger('selectedIndex', {
      selectedIndex: _this.selectedIndex
    });
  });
  this.hammer = phantasus.Util.hammer(this.canvas, ['pan'])
  .on(
    'panmove',
    this.panmove = function (event) {
      if (_this.selectedIndex !== -1) {
        var position = phantasus.CanvasUtil.getMousePos(
          event.target, event);
        var fraction = _this.fractionToStopPix
        .invert(position.x);
        fraction = Math.max(0, fraction);
        fraction = Math.min(1, fraction);
        _this.trigger('moved', {
          fraction: fraction
        });
      }
    }).on(
    'panstart',
    this.panstart = function (event) {
    }).on('panend', this.panend = function (event) {
    _this.selectedIndex = -1;
  });
  $(this.canvas).on('keydown', function (e) {
    // 8=backspace, 46=delete
    if ((e.which == 8 || e.which == 46) && _this.selectedIndex !== -1) {
      _this.trigger('delete');
      e.preventDefault();
      e.stopPropagation();
      e.stopImmediatePropagation();
    }
  });
};
phantasus.LegendWithStops.prototype = {
  border: 7,
  stopHalfSize: 5,
  selectedIndex: -1,
  destroy: function () {
    $(this.canvas).off('keyup').off('mousedown');
    this.hammer.off('panstart',
      this.panstart).off('panmove', this.panmove);
    this.hammer.destroy();
  },
  setSelectedIndex: function (index) {
    this.selectedIndex = index;
  },
  findIndexForPosition: function (position) {
    // pix - stopHalfSize to pix + stopHalfSize
    if (position.y >= 22) {
      for (var i = 0, length = this.fractions.length; i < length; i++) {
        var pix = this.fractionToStopPix(this.fractions[i]);
        var start = pix - this.stopHalfSize;
        var end = pix + this.stopHalfSize;
        if (position.x >= start && position.x <= end) {
          return i;
        }
      }
    }
    return -1;
  },
  draw: function (fractions, colors, stepped, fractionToStopPix) {
    this.fractions = fractions;
    this.colors = colors;
    this.stepped = stepped;
    this.fractionToStopPix = fractionToStopPix;
    var context = this.canvas.getContext('2d');
    phantasus.CanvasUtil.resetTransform(context);
    context.clearRect(0, 0, this.getUnscaledWidth(), this
      .getUnscaledHeight());
    context.translate(this.border, 0);
    phantasus.HeatMapColorSchemeLegend.draw(context, fractions, colors, this
        .getUnscaledWidth()
      - 2 * this.border, this.getUnscaledHeight() - 20, stepped);
    context.translate(-this.border, 0);
    context.lineWidth = 1;
    context.strokeStyle = 'Grey';
    context.strokeRect(this.border, 0, this.getUnscaledWidth() - 2
      * this.border, this.getUnscaledHeight() - 20);
    for (var i = 0; i < fractions.length; i++) {
      if (i > 0 && fractions[i] === fractions[i - 1]) {
        continue;
      }
      context.fillStyle = colors[i];
      var pix = fractionToStopPix(fractions[i]);
      context.fillRect(pix - this.stopHalfSize, 22,
        this.stopHalfSize * 2, this.stopHalfSize * 2);
      if (this.selectedIndex === i) {
        context.lineWidth = 2;
        context.strokeStyle = 'black';
      } else {
        context.lineWidth = 1;
        context.strokeStyle = 'Grey';
      }
      context.strokeRect(pix - this.stopHalfSize, 22,
        this.stopHalfSize * 2, this.stopHalfSize * 2);
    }
  }
};
phantasus.Util.extend(phantasus.LegendWithStops, phantasus.AbstractCanvas);
phantasus.Util.extend(phantasus.LegendWithStops, phantasus.Events);

phantasus.Popup = {};
phantasus.Popup.initted = false;
phantasus.Popup.init = function () {
  if (phantasus.Popup.initted) {
    return;
  }

  phantasus.Popup.initted = true;
  phantasus.Popup.$popupDiv = $(document.createElement('div'));
  phantasus.Popup.$popupDiv.css('position', 'absolute').css('zIndex', 1050).css('overflow', 'auto').addClass('dropdown clearfix');
  phantasus.Popup.$contextMenu = $(document.createElement('ul'));
  phantasus.Popup.$contextMenu.addClass('dropdown-menu').css('display',
    'block').css('position', 'static').css('margin-bottom', '5px');
  phantasus.Popup.$contextMenu.appendTo(phantasus.Popup.$popupDiv);
  phantasus.Popup.$contextMenu.on('click', 'a', function (e) {
    e.preventDefault();
    var $this = $(this);
    // if (!$this.hasClass('copy')) {
    phantasus.Popup.popupCallback(e, $this.data('name'));
    phantasus.Popup.hide();
    // }

  });
};

phantasus.Popup.popupInDom = false;
phantasus.Popup.hidePopupMenu = function (e) {
  if (phantasus.Popup.component == e.target) {
    e.preventDefault();
    e.stopPropagation();
  }
  phantasus.Popup.hide();
};
phantasus.Popup.hide = function () {
  phantasus.Popup.$popupDiv.hide();
  $(document.body).off('mousedown', phantasus.Popup.hidePopupMenu);
  phantasus.Popup.popupCallback = null;
  phantasus.Popup.component = null;
};

phantasus.Popup.showPopup = function (menuItems, position, component, callback) {
  phantasus.Popup.init();
  if (phantasus.Popup.component == component) {
    phantasus.Popup.hide();
    return;
  }
  phantasus.Popup.popupCallback = callback;
  phantasus.Popup.component = component;
  var html = [];
  for (var i = 0, length = menuItems.length; i < length; i++) {
    var item = menuItems[i];
    if (item.header) {
      html.push('<li role="presentation" class="dropdown-header">'
        + item.name + '</li>');
    } else if (item.separator) {
      html.push('<li class="divider"></li>');
    } else {
      html.push('<li role="presentation"');
      if (item.disabled) {
        html.push('class="disabled"');
      }
      html.push('><a data-name="' + item.name
        + '" data-type="popup-item" tabindex="-1" href="#"');
      if (item.class) {
        html.push(' class="' + item.class + '"');
      }
      html.push('>');
      if (item.checked) {
        html
          .push('<span class="dropdown-checkbox fa fa-check"></span>');
      }

      html.push(item.name);
      if (item.icon) {
        html.push('<span class="pull-right ' + item.icon + '"></span>');
      }
      html.push('</a>');

      html.push('</li>');
    }
  }
  phantasus.Popup.$contextMenu.html(html.join(''));
  if (!phantasus.Popup.popupInDom) {
    phantasus.Popup.popupInDom = true;
    phantasus.Popup.$popupDiv.appendTo($(document.body));
  }
  var $body = $(document.body);
  var $window = $(window);
  var windowWidth = $window.width();
  var windowHeight = $window.height();
  var popupWidth = phantasus.Popup.$popupDiv.width();
  var popupHeight = phantasus.Popup.$popupDiv.height();
  var left = position.x;
  var top = position.y;
  // default is bottom-right
  if ((left + popupWidth) >= windowWidth) { // offscreen right
    left -= popupWidth;
    left = Math.max(4, left);
  }
  if ((top + popupHeight) >= (windowHeight)) { // offscreen bottom
    top -= popupHeight;
    top = Math.max(4, top);
  }

  phantasus.Popup.$popupDiv.css({
    // height: popupHeight + 'px',
    display: 'block',
    left: left,
    top: top
  });

  phantasus.Popup.$popupDiv.show();

  $body.off('mousedown', phantasus.Popup.hidePopupMenu);
  window.setTimeout(function () {
    $body.on('mousedown', function (e) {
      var $target = $(e.target);
      if ($target[0] !== phantasus.Popup.$popupDiv[0] && $target.data('type') !== 'popup-item') {
        phantasus.Popup.hidePopupMenu(e);
      }
    });
  }, 1);
};

phantasus.RowDendrogram = function (heatMap, tree, positions, project) {
  phantasus.AbstractDendrogram.call(this, heatMap, tree, positions,
    project, phantasus.AbstractDendrogram.Type.ROW);
};
phantasus.RowDendrogram.prototype = {
  drawNode: function (context, node) {
    var radius = this.getNodeRadius(node);
    var pix = this.toPix(node);
    context.beginPath();
    context.arc(pix[0], pix[1], radius, Math.PI * 2, false);
    context.fill();
  },
  isDragHotSpot: function (p) {
    return Math.abs(this.scale(this.cutHeight) - p.x) <= 2;
  },
  drawCutSlider: function (clip, context) {
    if (context.setLineDash) {
      context.setLineDash([5]);
    }
    context.strokeStyle = 'black';
    var nx = this.scale(this.cutHeight);
    context.beginPath();
    context.moveTo(nx, clip.y);
    context.lineTo(nx, this.getUnscaledHeight());
    context.stroke();
    if (context.setLineDash) {
      context.setLineDash([]);
    }
  },
  getPreferredSize: function () {
    return {
      width: 100,
      height: Math.ceil(this.positions.getPosition(this.positions
            .getLength() - 1)
        + this.positions
          .getItemSize(this.positions.getLength() - 1))
    };
  },
  paintMouseOver: function (clip, context) {
    if (this.project.getHoverRowIndex() !== -1) {
      phantasus.CanvasUtil.resetTransform(context);
      context.translate(0, -clip.y);
      this.drawRowBorder(context, this.positions, this.project
        .getHoverRowIndex(), this.getUnscaledWidth());
    }
  },
  drawRowBorder: function (context, positions, index, gridSize) {
    var size = positions.getItemSize(index);
    var pix = positions.getPosition(index);
    // top and bottom lines
    context.beginPath();
    context.moveTo(0, pix + size);
    context.lineTo(gridSize, pix + size);
    context.stroke();
    context.beginPath();
    context.moveTo(0, pix);
    context.lineTo(gridSize, pix);
    context.stroke();
  },
  createScale: function () {
    return d3.scale.linear().domain([0, this.tree.maxHeight]).range(
      [this.getUnscaledWidth(), 0]);
  },
  getMaxIndex: function (clip) {
    return phantasus.Positions.getBottom(clip, this.positions);
  },
  getMinIndex: function (clip) {
    return phantasus.Positions.getTop(clip, this.positions);
  },
  toPix: function (node) {
    var min = this.positions.getPosition(node.minIndex)
      + this.positions.getItemSize(node.minIndex) / 2;
    var max = this.positions.getPosition(node.maxIndex)
      + this.positions.getItemSize(node.maxIndex) / 2;
    return [this.scale(node.height), (min + max) / 2];
  },
  drawPathFromNodeToParent: function (context, node) {
    var pix = this.toPix(node);
    var parentPix = this.toPix(node.parent);
    context.beginPath();
    context.moveTo(pix[0], pix[1]);
    context.lineTo(parentPix[0], pix[1]);
    context.lineTo(parentPix[0], parentPix[1]);
    context.stroke();
  },
  drawNodePath: function (context, node, minIndex, maxIndex) {
    var children = node.children;
    var left = children[0];
    var right = children[1];
    // set up points for poly line
    var ry = this.toPix(right)[1];
    var rx = this.scale(right.height);
    var ly = this.toPix(left)[1];
    var lx = this.scale(left.height);
    var nx = this.scale(node.height);
    var x;
    var y;
    if (!this.drawLeafNodes) {
      var leftIsLeaf = left.children !== undefined;
      var rightIsLeaf = right.children !== undefined;
      if (leftIsLeaf) {
        lx = nx + 4;
      }
      if (rightIsLeaf) {
        rx = nx + 4;
      }
      x = [rx, nx, nx, lx];
      y = [ry, ry, ly, ly];
    } else {
      x = [rx, nx, nx, lx];
      y = [ry, ry, ly, ly];
    }
    context.beginPath();
    context.moveTo(x[0], y[0]);
    for (var i = 1, length = x.length; i < length; i++) {
      context.lineTo(x[i], y[i]);
    }
    context.stroke();
  }
};
phantasus.Util.extend(phantasus.RowDendrogram, phantasus.AbstractDendrogram);

/**
 * @param model{phantasus.SelectionModel}
 */
phantasus.ScentedSearch = function (model, positions, isVertical, scrollbar,
                                   heatMap) {
  phantasus.AbstractCanvas.call(this, false);
  this.model = model;
  this.positions = positions;
  this.isVertical = isVertical;
  this.scrollbar = scrollbar;
  this.heatMap = heatMap;
  this.searchIndices = [];
  scrollbar.decorator = this;
  var _this = this;
  var mouseMove = function (e) {
    var indices = _this.getSearchIndices(e);

    document.body.style.cursor = indices.length === 0 ? 'default' : 'pointer';
    scrollbar.canvas.style.cursor = indices.length === 0 ? 'default' : 'pointer';
    var tipOptions = {
      event: e,
      heatMapLens: indices.length >= 0
    };
    if (isVertical) {
      heatMap.setToolTip(indices.length >= 0 ? indices : null,
        -1, tipOptions);
    } else {
      heatMap.setToolTip(-1, indices.length >= 0 ? indices
        : null, tipOptions);
    }

  };
  var mouseExit = function (e) {
    // need to set body cursor b/c mouse can be partially on the scroll bar,
    // but the canvas cursor has no effect
    document.body.style.cursor = 'default';
    scrollbar.canvas.style.cursor = 'default';
    heatMap.setToolTip(-1, -1, {event: e});
  };
  $(scrollbar.canvas).on('mousemove.phantasus', mouseMove).on('mouseout.phantasus', mouseExit);
};

phantasus.ScentedSearch.LINE_HEIGHT = 3.5;
phantasus.ScentedSearch.prototype = {
  mouseMovedIndex: -1,
  getIndex: function (event) {
    var pix = phantasus.CanvasUtil.getMousePos(event.target, event);
    var val = pix[this.isVertical ? 'y' : 'x'];
    return this.getIndexForPix(val);
  },
  getSearchIndices: function (event) {
    var pix = phantasus.CanvasUtil.getMousePos(event.target, event);
    var val = pix[this.isVertical ? 'y' : 'x'];
    return this.getSearchIndicesForPix(val);
  },
  getSearchIndicesForPix: function (pix) {
    var indices = this.searchIndices;
    if (indices == null) {
      return [];
    }
    var scale = this.scale;
    var tolerance = phantasus.ScentedSearch.LINE_HEIGHT;
    var matches = [];
    for (var i = 0, length = indices.length; i < length; i++) {
      var midVal = this.positions.getPosition(indices[i]) * scale;
      if (Math.abs(midVal - pix) <= tolerance) {
        matches.push(indices[i]);
      }
    }
    return matches;
  },
  getIndexForPix: function (pix) {
    var indices = this.searchIndices;
    if (indices == null) {
      return -1;
    }
    var tolerance = phantasus.ScentedSearch.LINE_HEIGHT;
    if (this.mouseMovedIndex > 0) {
      var midVal = this.positions
          .getPosition(indices[this.mouseMovedIndex])
        * scale;
      if (Math.abs(midVal - pix) <= tolerance) {
        return this.mouseMovedIndex;
      }
    }
    var low = 0;
    var scale = this.scale;
    var high = indices.length - 1;

    while (low <= high) {
      var mid = (low + high) >> 1;
      var midVal = this.positions.getPosition(indices[mid]) * scale;
      var cmp = 0;
      if (Math.abs(midVal - pix) <= tolerance) {
        cmp = 0;
      } else if (midVal < pix) {
        cmp = -1; // Neither val is NaN, thisVal is smaller
      } else if (midVal > pix) {
        cmp = 1; // Neither val is NaN, thisVal is larger
      }
      if (cmp < 0)
        low = mid + 1;
      else if (cmp > 0)
        high = mid - 1;
      else
        return mid; // key found
    }
    return -1; // -(low + 1); // key not found.

  },
  tap: function (position) {
    var val = position[this.isVertical ? 'y' : 'x'];
    var index = this.getIndexForPix(val);
    this.scrollbar.canvas.style.cursor = index < 0 ? 'default' : 'pointer';
    if (index >= 0) {
      if (this.isVertical) {
        this.heatMap.scrollTop(this.positions
          .getPosition(this.searchIndices[index]));
      } else {
        this.heatMap.scrollLeft(this.positions
          .getPosition(this.searchIndices[index]));
      }
      return true;
    }
    return false;
  },
  update: function () {
    this.searchIndices = this.model.getViewIndices().values().sort(
      function (a, b) {
        return a < b ? -1 : 1;
      });
  },
  draw: function (clip, context) {
    var width = this.scrollbar.getUnscaledWidth();
    var height = this.scrollbar.getUnscaledHeight();
    var availableLength = ((this.isVertical ? height : width))
      - phantasus.ScentedSearch.LINE_HEIGHT;
    this.scale = availableLength
      / (this.positions.getPosition(this.positions.getLength() - 1) + this.positions
        .getItemSize(this.positions.getLength() - 1));
    context.fillStyle = phantasus.ScentedSearch.TICK_COLOR;
    context.lineWidth = 1;
    this.drawIndices(context, this.searchIndices);
    this.drawHoverMatchingValues(context);
  },
  drawHoverMatchingValues: function (context) {
    var heatmap = this.heatMap;
    context.fillStyle = phantasus.ScentedSearch.MATCHING_VALUES_TICK_COLOR;
    if (heatmap.mousePositionOptions
      && heatmap.mousePositionOptions.name != null) {
      var isColumns = !this.isVertical;
      var track = heatmap.getTrack(heatmap.mousePositionOptions.name,
        isColumns);
      if (track == null) {
        return;
      }
      if (track.settings.highlightMatchingValues) {
        var hoverIndex = isColumns ? heatmap.getProject()
          .getHoverColumnIndex() : heatmap.getProject()
          .getHoverRowIndex();
        if (hoverIndex === -1) {
          return;
        }
        var vector = track.getVector();
        var value = vector.getValue(hoverIndex);
        var valueToModelIndices = track.getFullVector().getProperties()
          .get(phantasus.VectorKeys.VALUE_TO_INDICES);
        if (!valueToModelIndices) {
          var fullVector = track.getFullVector();
          valueToModelIndices = phantasus.VectorUtil
            .createValueToIndicesMap(fullVector);
          fullVector.getProperties().set(
            phantasus.VectorKeys.VALUE_TO_INDICES,
            valueToModelIndices);

        }
        var modelIndices = valueToModelIndices.get(value);
        if (modelIndices == null) {
          // console.log('valueToModelIndices error');
          return;
        }
        var scale = this.scale;
        var lineLength = !this.isVertical ? this.scrollbar
          .getUnscaledHeight() : this.scrollbar
          .getUnscaledWidth();
        var isVertical = this.isVertical;
        var positions = this.positions;
        var project = heatmap.getProject();
        for (var i = 0, length = modelIndices.length; i < length; i++) {
          var modelIndex = modelIndices[i];
          var index = isVertical ? project
            .convertModelRowIndexToView(modelIndex) : project
            .convertModelColumnIndexToView(modelIndex);
          if (index === -1) {
            continue;
          }
          var pix = positions.getPosition(index) * scale;
          if (isVertical) {
            context.fillRect(0, pix, lineLength,
              phantasus.ScentedSearch.LINE_HEIGHT);
          } else {
            context.fillRect(pix, 0,
              phantasus.ScentedSearch.LINE_HEIGHT, lineLength);

          }
        }
      }

    }
  },
  drawIndices: function (context, highlightedIndices) {
    var scale = this.scale;
    var lineLength = !this.isVertical ? this.scrollbar.getUnscaledHeight()
      : this.scrollbar.getUnscaledWidth();

    var isVertical = this.isVertical;
    var positions = this.positions;
    for (var i = 0, length = highlightedIndices.length; i < length; i++) {
      var index = highlightedIndices[i];
      var pix = positions.getPosition(index) * scale;
      if (isVertical) {
        context.beginPath();
        context.rect(0, pix, lineLength,
          phantasus.ScentedSearch.LINE_HEIGHT);
        context.fill();
        //  context.stroke();

      } else {
        context.beginPath();
        context.rect(pix, 0, phantasus.ScentedSearch.LINE_HEIGHT,
          lineLength);
        context.fill();
        //context.stroke();
      }
    }

  }
};
phantasus.Util.extend(phantasus.ScentedSearch, phantasus.AbstractCanvas);
phantasus.ScentedSearch.MATCHING_VALUES_TICK_COLOR = 'black';
phantasus.ScentedSearch.TICK_COLOR = '#3182bd';

phantasus.ScrollBar = function (isVertical) {
  phantasus.AbstractCanvas.call(this);
  this.isVertical = isVertical;
  $(this.canvas).css('border', '1px solid #d8d8d8');
  if (isVertical) {
    this.setBounds({
      width: 12
    });
  } else {
    this.setBounds({
      height: 12
    });
  }
  this.field = this.isVertical ? 'y' : 'x';
  var that = this;
  var mouseMove = function (event) {
    if (!phantasus.CanvasUtil.dragging) {
      var position = phantasus.CanvasUtil.getMousePos(event.target, event,
        true);
      var mouseOver = (position[that.field] >= that.thumbPos && position[that.field] <= (that.thumbPos + that.thumbExtent));
      if (that.thumbMouseOver !== mouseOver) {
        that.thumbMouseOver = mouseOver;
        that.repaint();
      }
    }
  };
  var mouseExit = function (e) {
    if (!phantasus.CanvasUtil.dragging && that.thumbMouseOver) {
      that.thumbMouseOver = false;
      that.repaint();
    }
  };
  $(this.canvas).on('mousemove', mouseMove).on('mouseout', mouseExit).on(
    'mouseenter', mouseMove);
  this.hammer = phantasus.Util
    .hammer(this.canvas, [this.isVertical ? 'panv' : 'panh', 'tap'])
    .on(
      'panstart',
      this.panstart = function (event) {
        var position = phantasus.CanvasUtil.getMousePos(
          event.target, event, true);
        if (position[that.field] >= that.thumbPos
          && position[that.field] <= (that.thumbPos + that.thumbExtent)) {
          that.draggingThumb = true;
          that.dragStartThumbPos = that.thumbPos;
        } else {
          that.draggingThumb = false;
        }
      })
    .on('panend', this.panend = function (event) {
      that.draggingThumb = false;
    })
    .on(
      'panmove',
      this.panmove = function (event) {
        if (that.draggingThumb) {
          var position = phantasus.CanvasUtil.getMousePos(
            event.target, event);
          var thumbPosPix = that.dragStartThumbPos
            + (that.isVertical ? event.deltaY
              : event.deltaX);
          var f = thumbPosPix
            / (that.visibleExtent - that.thumbExtent);
          var value = f * that.maxValue;
          // convert pix to value
          that.setValue(value, true);
          event.preventDefault();
          event.srcEvent.stopPropagation();
          event.srcEvent.stopImmediatePropagation();
        }
      })
    .on(
      'tap doubletap',
      this.tap = function (event) {
        // ensure not clicked on the thumb
        if (!that.draggingThumb) {
          var position = phantasus.CanvasUtil.getMousePos(
            event.target, event);
          if (!that.decorator.tap(position)) {
            // scroll up or down by thumbExtent
            var thumbExtentToValue = (that.thumbExtent / that.totalExtent)
              * that.totalExtent;
            that.scrollToTop = position[that.field] < that.thumbPos;
            that.setValue(that.scrollToTop ? that.value
            - thumbExtentToValue : that.value
            + thumbExtentToValue, true);
          }
        }
      });
};
phantasus.ScrollBar.prototype = {
  thumbPos: 0, // the top of the thumb, from 0 to visibleExtent-thumbExtent
  thumbExtent: 0,
  extent: 0,
  value: 0, // from 0 to totalExtent-extent
  maxValue: 0, // totalExtent-extent
  totalExtent: 0,
  visibleExtent: 0,
  dragStartThumbPos: 0,
  draggingThumb: false,
  thumbMouseOver: false,
  dispose: function () {
    phantasus.AbstractCanvas.prototype.dispose.call(this);
    this.hammer.off('panend', this.panend).off('panstart',
      this.panstart).off('panmove', this.panmove).off('tap', this.tap).off('doubletap', this.tap);
    this.hammer.destroy();
  },
  draw: function (clip, context) {
    var width = this.getUnscaledWidth();
    var height = this.getUnscaledHeight();
    if (this.visibleExtent === this.totalExtent) {
      context.clearRect(0, 0, width, height);
    } else {
      context.fillStyle = 'rgb(241,241,241)';
      context.fillRect(0, 0, width, height);
      context.fillStyle = !this.thumbMouseOver ? 'rgb(137,137,137)'
        : 'rgb(100,100,100)';
      if (this.isVertical) {
        context.fillRect(0, this.thumbPos, width, this.thumbExtent);
      } else {
        context.fillRect(this.thumbPos, 0, this.thumbExtent, height);
      }
    }
    this.decorator.draw(clip, context);
  },
  setThumbPosFromValue: function () {
    // value is thumb top position
    var f = this.maxValue == 0 ? 0 : this.value / this.maxValue;
    this.thumbPos = f * (this.visibleExtent - this.thumbExtent);
    this.thumbPos = Math.max(0, this.thumbPos);
  },
  getValue: function () {
    return this.value;
  },
  getMaxValue: function () {
    return this.maxValue;
  },
  setValue: function (value, trigger) {
    if (isNaN(value)) {
      value = 0;
    }
    if (this.visibleExtent === this.totalExtent) {
      value = 0;
    }
    value = Math.max(value, 0);
    value = Math.min(this.maxValue, value);
    this.value = value;
    this.setThumbPosFromValue();
    if (trigger) {
      this.trigger('scroll', {value: this.value});
      this.repaint();
    }
    return this.value;
  },
  setTotalExtent: function (totalExtent) {
    this.totalExtent = totalExtent;
    this._setRange();
  },
  getTotalExtent: function () {
    return this.totalExtent;
  },
  getVisibleExtent: function () {
    return this.visibleExtent;
  },
  _setRange: function () {
    this.thumbExtent = Math.max(10, this.visibleExtent
      * (this.visibleExtent / this.totalExtent));
    this.maxValue = this.totalExtent - this.visibleExtent;
    this.maxValue = Math.max(0, this.maxValue);
    if (this.isVertical) {
      this.setBounds({
        height: this.visibleExtent
      });
    } else {
      this.setBounds({
        width: this.visibleExtent
      });
    }
  },
  setExtent: function (visibleExtent, totalExtent, value) {
    this.visibleExtent = visibleExtent;
    this.totalExtent = totalExtent;
    this._setRange();
    this.setValue(value, false);
  }
};
phantasus.Util.extend(phantasus.ScrollBar, phantasus.AbstractCanvas);
phantasus.Util.extend(phantasus.ScrollBar, phantasus.Events);

phantasus.ShapeChooser = function (options) {
  var formBuilder = new phantasus.FormBuilder();
  var map = options.map;
  var html = ['<select name="valuePicker" class="selectpicker" data-live-search="true">'];
  map.forEach(function (val, key) {
    html.push('<option');
    html.push(' value="');
    html.push(key);
    html.push('">');
    html.push(key);
    html.push('</option>');
  });
  html.push('</select>');
  formBuilder.append({
    name: 'selected_value',
    type: 'custom',
    value: html.join('')
  });

  var shapeField = new phantasus.ShapeField({showNone: true});

  formBuilder.append({
    style: 'max-width:50px;',
    name: 'selected_shape',
    type: 'custom',
    value: '<div data-name="shape"></div>'
  });
  shapeField.$el.appendTo(formBuilder.$form.find('[data-name=shape]'));

  var $valuePicker = formBuilder.$form.find('[name=valuePicker]');
  var selectedVal = $valuePicker.val();
  var _this = this;

  shapeField.setShapeValue(map.get(selectedVal));
  shapeField.on('change', function (e) {
    map.set(selectedVal, e.shape);
    _this.trigger('change', {
      value: selectedVal,
      shape: e.shape
    });

  });
  $valuePicker.selectpicker().change(function () {
    selectedVal = $valuePicker.val();
    shapeField.setShapeValue(map.get(selectedVal));
  });
  this.$div = formBuilder.$form;
};

phantasus.ShapeChooser.prototype = {};

phantasus.Util.extend(phantasus.ShapeChooser, phantasus.Events);

/**
 *
 * @param options.shapes Array of shape names or null to use phantasus.VectorShapeModel.SHAPES
 * @param options.showNone Whether none should be an option
 * @constructor
 */
phantasus.ShapeField = function (options) {
  var shapes = options.shapes || phantasus.VectorShapeModel.SHAPES;

  var _this = this;
  var html = [];
  var size2 = 8;
  var x = 4;
  var y = 4;
  html
    .push('<div style="margin-bottom:1em;" class="btn-group">');
  html
    .push(
      '<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false"><span data-name="selection"></span> <span class="fa fa-caret-down"></span></button>');
  html.push('<ul class="dropdown-menu" role="menu">');
  if (options.showNone) {
    html.push('<li><a data-name="none" href="#">(None)</a></li>');
  }
  for (var i = 0; i < shapes.length; i++) {
    var context = new C2S(size2 * 2, size2 * 2);
    context.translate(4, 4);
    phantasus.CanvasUtil.drawShape(context, shapes[i], x, y, size2);
    var svg = context.getSerializedSvg();
    html.push('<li><a data-name="' + shapes[i] + '" href="#">' + svg
      + '</a></li>');
  }

  html.push('</ul></div>');
  var $el = $(html.join(''));
  var $header = $el.find('[data-name=selection]');
  $el.on('click', 'li > a', function (e) {
    e.preventDefault();
    var shape = $(this).data('name');
    setShapeValue(shape);
    _this.trigger('change', {
      shape: shape
    });
  });
  var setShapeValue = function (val) {
    if (val === 'none') {
      $header.html('(None)');
    } else {
      var context = new C2S(size2 * 2, size2 * 2);
      context.translate(4, 4);
      phantasus.CanvasUtil.drawShape(context, val, x, y, size2);
      $header.html(context.getSerializedSvg());
    }
  };
  this.setShapeValue = setShapeValue;
  this.$el = $el;
};
phantasus.ShapeField.prototype = {};
phantasus.Util.extend(phantasus.ShapeField, phantasus.Events);

phantasus.SortByValuesIndicator = function (project, isVertical, positions) {
  phantasus.AbstractCanvas.call(this, true);
  this.project = project;
  this.isVertical = isVertical;
  this.positions = positions;
  this.lastPosition = {
    start: -1,
    end: -1
  };
};
phantasus.SortByValuesIndicator.prototype = {
  prePaint: function (clip, context) {
    var positions = this.positions;
    var start = 0;
    var end = positions.getLength();
    if (!this.isVertical) {
      start = phantasus.Positions.getLeft(clip, positions);
      end = phantasus.Positions.getRight(clip, positions);
    } else {
      start = phantasus.Positions.getTop(clip, positions);
      end = phantasus.Positions.getBottom(clip, positions);
    }
    if (this.invalid || start !== this.lastPosition.start
      || end !== this.lastPosition.end) {
      this.lastPosition.start = start;
      this.lastPosition.end = end;
      this.invalid = true;
    }
  },
  draw: function (clip, context) {
    var project = this.project;
    var isVertical = this.isVertical;
    var positions = this.positions;
    var sortKeys = isVertical ? project.getColumnSortKeys() : project
    .getRowSortKeys();
    context.translate(-clip.x, -clip.y);
    context.fillStyle = 'black';
    context.textBaseline = 'top';
    context.textAlign = 'left';
    context.font = '8px ' + phantasus.CanvasUtil.getFontFamily(context);
    var start = 0;
    var end = positions.getLength();
    if (!isVertical) {
      start = phantasus.Positions.getLeft(clip, positions);
      end = phantasus.Positions.getRight(clip, positions);
    } else {
      start = phantasus.Positions.getTop(clip, positions);
      end = phantasus.Positions.getBottom(clip, positions);
    }
    var arrowWidth = 3;
    var arrowHeight = 4;
    for (var i = 0; i < sortKeys.length; i++) {
      var key = sortKeys[i];
      if (key instanceof phantasus.SortByValuesKey) { // are we sorting
        // columns by the
        // values in a row?

        var modelIndices = key.modelIndices;
        for (var j = 0; j < modelIndices.length; j++) {
          var modelIndex = modelIndices[j];
          var view = isVertical ? project
          .convertModelRowIndexToView(modelIndex) : project
          .convertModelColumnIndexToView(modelIndex);
          if (view !== -1 && view >= start && view < end) {
            context.save();
            var pix = positions.getPosition(view);
            var size = positions.getItemSize(view);
            if (!isVertical) {
              context.translate(pix + size / 2, 0);
            } else {
              context.translate(2, pix + size / 2);
            }
            context.beginPath();
            // if (!isVertical) {
            if (key.getSortOrder() === phantasus.SortKey.SortOrder.ASCENDING) {
              // up arrow
              context.moveTo(0, 0);
              context.lineTo(arrowWidth, arrowHeight);
              context.lineTo(-arrowWidth, arrowHeight);
            } else if (key.getSortOrder() === phantasus.SortKey.SortOrder.DESCENDING) { // down
              // arrow
              context.moveTo(0, arrowHeight);
              context.lineTo(arrowWidth, 0);
              context.lineTo(-arrowWidth, 0);
            } else { // diamond
              context.moveTo(0, 0);
              context.lineTo(arrowWidth, arrowHeight / 2);
              context.lineTo(0, arrowHeight);
              context.lineTo(-arrowWidth, arrowHeight / 2);

            }
            // } else {
            // if (!ascending) { // left arrow
            // context.moveTo(0, 0);
            // context.lineTo(arrowWidth, arrowHeight);
            // context.lineTo(arrowWidth, -arrowHeight);
            // } else {
            // context.moveTo(arrowWidth, 0); // right arrow
            // context.lineTo(0, arrowHeight);
            // context.lineTo(0, -arrowHeight);
            // }
            // }
            context.fill();

            // don't indicate sort priority b/c of limited space
//						if (sortKeys.length > 1) {
//							context.fillText('' + (i + 1), 0, 0);
//						}
            context.restore();
          }
        }
      }
    }
  }
};
phantasus.Util.extend(phantasus.SortByValuesIndicator, phantasus.AbstractCanvas);

phantasus.SortDialog = function (project) {
  var _this = this;
  // choose rows or columns
  var $chooserDiv = $('<div class="container-fluid"></div>');
  var $div = $('<div class="container-fluid"></div>');
  var html = [];
  html
    .push('<div style="border-bottom:1px solid LightGrey;margin-bottom:20px;" class="row">');
  html.push('<form class="form-horizontal" role="form">');
  html
    .push('<div class="col-xs-2"><label class="control-label">Sort</label></div>');
  html.push('<div class="col-xs-5">');
  html
    .push('<div class="radio"><label><input type="radio" name="rowsOrColumns" value="rows" checked>Rows</label></div>');
  html
    .push('<div class="radio"><label><input type="radio" name="rowsOrColumns" value="columns">Columns</label></div>');
  html.push('</div>');
  html.push('</form>');
  html.push('</div>');
  $chooserDiv.html(html.join(''));

  function toggle(isColumns) {
    _this.isColumns = isColumns;
    var $element = _this.build(project, isColumns);
    $div.empty().html($element);
    $div.on('click', '[data-name=delete]', function (e) {
      var $this = $(this);
      e.preventDefault();
      $this.closest('div.row').remove();
    });
    $div.on('click', '[data-name=add]', function (e) {
      var $this = $(this);
      var level = [];
      var $sibling = $this.closest('div.row');
      _this.createLevel(level, new phantasus.SortKey('',
        phantasus.SortKey.SortOrder.ASCENDING), _this.fields);
      $sibling.after($(level.join('')));
      e.preventDefault();
    });
  }

  $chooserDiv.on('change', '[name=rowsOrColumns]', function (e) {
    var $this = $(this);
    toggle($this.val() === 'columns');
  });
  toggle(false);
  var $outer = $('<div></div>');
  $chooserDiv.appendTo($outer);
  $div.appendTo($outer);
  phantasus.FormBuilder
  .showOkCancel({
    title: 'Sort',
    content: $outer,
    okCallback: function () {
      var $forms = $div.find('form');
      var sortBy = $forms.find('[name=sortBy]').map(function () {
        return $(this).val();
      });
      var lockOrder = $forms.find('[name=lockOrder]').map(function () {
          return $(this).prop('checked');
        });
        var sortOrder = $forms.find('[name=sortOrder]:checked')
      .map(function () {
        return $(this).val();
      });

      var groupBy = $div.find('[name=groupBy]').val();
      var newSortKeys = [];
      var modelIndices = _this.isColumns ? project
      .getRowSelectionModel().toModelIndices() : project
      .getColumnSelectionModel().toModelIndices();
      var existingSortKeys = _this.isColumns ? project
      .getColumnSortKeys() : project.getRowSortKeys();
      for (
      var i = 0; i <
        existingSortKeys.length; i++) {
        // delete existing sort keys that were locked and were deleted by user
      if (existingSortKeys[i].isUnlockable()) {
        existingSortKeys.splice(i, 1) ;
          i--;
          }
        }

      var newSortKeyFields = new phantasus.Set();
      for (var i = 0; i < sortBy.length; i++) {
        if (!newSortKeyFields.has(sortBy[i])) {// don't add 2x
          newSortKeyFields.add(sortBy[i]);
          var key = null;if (sortBy[i] === 'selection') {
            key =new phantasus.SortByValuesKey(
              modelIndices, sortOrder[i],
              _this.isColumns);
          } else if (sortBy[i] !== '') {
            key =new phantasus.SortKey(
              sortBy[i], sortOrder[i]);}
            if (key != null) {
              newSortKeys.push(key);
              if (lockOrder[i]) {
                key.setLockOrder(1);
              }
          }
        if (key != null) {
              newSortKeys.push(key);
              if (lockOrder[i]) {
                key.setLockOrder(1);
              }
            }}
      }
      var newGroupKeys = [];
      if (groupBy != null) {
        for (var i = 0; i < groupBy.length; i++) {
          newGroupKeys.push(new phantasus.SortKey(groupBy[i],
            phantasus.SortKey.SortOrder.UNSORTED));
        }
      }

        if (_this.isColumns) {
          project.setGroupColumns(newGroupKeys, true);
          project.setColumnSortKeys(phantasus.SortKey
            .keepExistingSortKeys(newSortKeys, existingSortKeys), true);
        } else {
          project.setGroupRows(newGroupKeys, true);
          project.setRowSortKeys(phantasus.SortKey
            .keepExistingSortKeys(newSortKeys, existingSortKeys), true);
        }
      }
    });
};
phantasus.SortDialog.prototype = {
  isColumns: false,
  build: function (project, isColumns) {
    var fields = phantasus.MetadataUtil.getMetadataNames(isColumns ? project
      .getFullDataset().getColumnMetadata() : project
      .getFullDataset().getRowMetadata());
    this.fields = fields;
    var html = [];
    var sortKeys = isColumns ? project.getColumnSortKeys() : project
      .getRowSortKeys();

    this.createLevel0(html);
    for (var i = 0; i < sortKeys.length; i++) { // add existing keys
      if (sortKeys[i].isUnlockable()) {
        this.createLevel(html, sortKeys[i], fields);
      }
    }
    // group by
    html.push('<div class="row">');
    html
      .push('<form class="form-horizontal" role="form">');
    html.push('<div class="col-xs-2"><label>Group by</label></div>');
    html.push('<div class="col-xs-4">');
    var groupByKeys = (isColumns ? project.getGroupColumns() : project
      .getGroupRows()).map(function (key) {
      return key.field;
    });

    html.push('<select multiple name="groupBy" class="selectpicker form-control">');
    _.each(fields, function (field) {
      html.push('<option value="' + field + '"');
      if (_.indexOf(groupByKeys, field) !== -1) {
        html.push(' selected');
      }
      html.push('>');
      html.push(field);
      html.push('</option>');
    });
    html.push('</select>');
    html.push('</div>');
    html.push('</div>');
    var $div = $(html.join(''));
    $div.find('.selectpicker').selectpicker({
      iconBase: 'fa',
      tickIcon: 'fa-check',
      style: 'btn-default btn-sm'
    });
    return $div;
  },
  createLevel0: function (html) {
    html
      .push('<div style="border-bottom:1px solid LightGrey;margin-bottom:20px;" class="row">');
    html.push('<form class="form-horizontal" role="form">');
    html.push('<div class="col-xs-8">');
    html.push('<a data-name="add" href="#">Add sort level</a>');
    html.push('</div>');
    html.push('</form>');
    html.push('</div>');
  },
  createLevel: function (html, key, fields) {
    html
      .push('<div style="border-bottom:1px solid LightGrey;margin-bottom:20px;" class="row">');
    html.push('<form class="form-horizontal" role="form">');
    html
      .push('<div class="col-xs-2"><label class="control-label">Sort by</label></div>');
    html.push('<div class="col-xs-4">');
    html.push('<select name="sortBy" class="form-control">');
    html.push('<option value=""></option>');
    html.push('<option value="selection"'
      + (key instanceof phantasus.SortByValuesKey ? ' selected' : '')
      + '>selection</option>');
    _.each(fields, function (field) {
      html.push('<option value="' + field + '"');
      if (field == key.toString()) {
        html.push(' selected');
      }
      html.push('>');
      html.push(field);
      html.push('</option>');
    });
    html.push('</select>');
    html.push('</div>');
    html.push('<div class="col-xs-5">');
    html
      .push('<div class="radio"><label><input type="radio" name="sortOrder" value="ascending"'
        + (phantasus.SortKey.SortOrder.ASCENDING == key
          .getSortOrder() ? ' checked' : '')
        + '>Ascending</label></div>');
    html
      .push('<div class="radio"><label><input type="radio" name="sortOrder" value="descending"'
        + (phantasus.SortKey.SortOrder.DESCENDING == key
          .getSortOrder() ? ' checked' : '')
        + '>Descending</label></div>');
    html.push('</div>');
    html.push('<div class="col-xs-1">');
    html.push('<a data-name="delete">Delete</a>');
    html.push('</div>');
    html.push('<div class="col-xs-12">');
    html.push('<div class="checkbox"><label><input name="lockOrder" type="checkbox"' + (key.getLockOrder() !== 0 ? ' checked' : '') + '> Lock sort level</label></div>');
    html.push('</div>');
    html.push('<div class="col-xs-12">');
    html.push('<br />');
    html.push('<a data-name="add" href="#">Add sort level</a>');
    html.push('</div>');
    html.push('</form>');
    html.push('</div>');
  }
};

phantasus.SteppedColorSupplier = function () {
  phantasus.AbstractColorSupplier.call(this);
  this.hiddenValue = 0;
  this.hiddenValues = new phantasus.Set();
  this.stepped = true;
};
/**
 * Convert value from input data range of input0 to input1 to pixel range of
 * pix0, pix1.
 *
 * @return The converted value.
 */
phantasus.SteppedColorSupplier.linearScale = function (value, input0, input1,
                                                      pix0, pix1) {
  return (value - input0) / (input1 - input0) * (pix1 - pix0) + pix0;
};
phantasus.SteppedColorSupplier.prototype = {
  createInstance: function () {
    return new phantasus.SteppedColorSupplier();
  },
  isStepped: function () {
    return true;
  },
  getHiddenValues: function () {
    return this.hiddenValues;
  },
  getIndexForFraction: function (f) {
    var fractions = this.fractions;
    if (f <= fractions[0]) {
      return 0;
    }
    if (f >= fractions[fractions.length - 1]) {
      return fractions.length - 1;
    }
    // Intervals exclude right end point and include left end point except
    // for the highest interval which includes everything > min
    for (var i = 0; i < fractions.length - 1; i++) {
      var left = fractions[i];
      var right = fractions[i + 1];
      if (f >= left && f < right) {
        return i;
      }
    }
    return fractions.length - 1;
  },
  getColor: function (row, column, value) {
    if (this.hiddenValues.has(value)) {
      value = this.hiddenValue;
    }
    if (isNaN(value)) {
      return this.missingColor;
    }
    var min = this.min;
    var max = this.max;
    var colors = this.colors;
    if (value <= min) {
      return colors[0];
    } else if (value >= max) {
      return colors[colors.length - 1];
    }
    var fraction = phantasus.SteppedColorSupplier.linearScale(value, min,
        max, 0, 100) / 100;
    return colors[this.getIndexForFraction(fraction)];
  }
};
phantasus.Util.extend(phantasus.SteppedColorSupplier,
  phantasus.AbstractColorSupplier);

/**
 * @param options.autohideTabBar
 *            Whether to autohide the tab bar when only 1 tab showing
 * @param options.landingPage Landing page to show when all tabs are closed
 */
phantasus.TabManager = function (options) {
  this.options = $.extend({}, {
    autohideTabBar: false,
    rename: true
  }, options);
  var _this = this;
  this.activeTabObject = null;
  this.activeTabId = null;
  this.idToTabObject = new phantasus.Map();
  this.$nav = $('<ul class="nav nav-tabs compact phantasus-nav"></ul>');
  this.$nav.sortable({
    containment: 'parent',
    axis: 'x',
    helper: 'clone',
    cancel: 'li:not(.phantasus-sortable)',
    items: 'li.phantasus-sortable'
  });
  this.$nav.sortable('disable');
  this.$nav.on('click', 'li > a', function (e) {
    var tabId = $(this).data('link');
    if (tabId != null) {
      e.preventDefault();
      if (_this.activeTabId !== tabId) {
        $(this).tab('show');
      }
    }
  });
  if (this.options.autohideTabBar) {
    this.$nav.css('display', 'none');
  }
  if (options.dropTab) {
    var html = [];
    html.push('<li class="phantasus-tab-addon dropdown pull-right tabdrop">');
    html.push('<div class="btn-group">');
    html.push('<button type="button" class="phantasus-drop-tab-toggle btn btn-link' +
      ' dropdown-toggle"' +
      ' data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">');
    html.push(' <span class="fa fa-angle-double-down"></span>');
    html.push('</button>');
    html
      .push('<ul class="dropdown-menu dropdown-menu-right" role="menu">');
    html.push('</ul>');
    html.push('</div>');
    html.push('</li>');
    var $tabDrop = $(html.join(''));
    // $tabDrop.css('display', 'none');
    var $tabDropMenu = $tabDrop.find('.dropdown-menu');
    $tabDrop.appendTo(this.$nav);
    var updateDropTab = function () {
      var totalWith = _this.$nav.width() - 17; // 17=width of dropdown
      var sum = 0;
      var tabDropItems = [];
      _this.$nav.find('> li').each(function () {
        var $li = $(this);
        var $a = $li.find('a');
        if (!$li.hasClass('phantasus-tab-addon')) {
          var title = $a.contents().first().text();
          var isActive = $li.hasClass('active');
          var href = $a.attr('href');
          tabDropItems.push('<li class="' + (isActive ? 'active' : '') + '"><a data-link="' + href.substring(1) + '" data-toggle="tab"' +
            ' href="' + href + '">' + title + '</a></li>');
          sum += $li.outerWidth();
          if (sum >= totalWith) {
            $li.css('display', 'none');
          } else {
            $li.css('display', '');
          }
        }
      });
      $tabDrop.css('display', tabDropItems.length > 0 ? '' : 'none');
      $tabDropMenu.html(tabDropItems.join(''));
    };
    $tabDrop.css('display', 'none');
    this.$nav.on('sortstop', function (event, ui) {
      updateDropTab();
    });
    $(window).on('resize', updateDropTab);
    this.$nav.on('remove', function () {
      $(window).off('resize', updateDropTab);
    });
    this.on('add remove rename reorder change', function () {
      updateDropTab();

    });
  }

  this.$nav.on('dblclick', 'li > a', function (e) {
    e.preventDefault();
    var $a = $(this);
    var $li = $a.parent('li');
    if ($li.hasClass('phantasus-tab-addon')) {
      return;
    }
    _this.rename($a.data('link'));

  });
  this.$nav.on('contextmenu.phantasus', 'li > a', function (e) {
    e.preventDefault();
    e.stopPropagation();
    e.stopImmediatePropagation();
    var $a = $(this);
    var $li = $a.parent('li');
    if ($li.hasClass('phantasus-tab-addon')) {
      return;
    }
    var menuItems = [];
    if ($a.data('phantasus-rename') && _this.options.rename) {
      menuItems.push({name: 'Rename'});
    }
    if ($a.data('phantasus-pin')) { // pinned
      menuItems.push({name: 'Unpin tab'});
    } else {
      menuItems.push({name: 'Pin tab'});
    }

    if (menuItems.length > 0) {
      phantasus.Popup.showPopup(menuItems, {
        x: e.pageX,
        y: e.pageY
      }, e.target, function (event, item) {
        if (item === 'Rename') {
          _this.rename($a.data('link'));
        } else if (item === 'Pin tab') {
          $a.data('phantasus-pin', true);
          $li.removeClass('phantasus-sortable');
          $li.detach();
          _this.$nav.prepend($li);
          $a.find('.close').hide();    // hide close button
          _this.$nav.sortable('option', 'items', 'li.phantasus-sortable');
          _this.$nav.sortable('refresh');
        } else if (item === 'Unpin tab') {
          $a.data('phantasus-pin', false);
          $li.addClass('phantasus-sortable');
          $a.find('.close').show(); // show close button
          _this.$nav.sortable('option', 'items', 'li.phantasus-sortable');
          _this.$nav.sortable('refresh');

        }
      });
    }
    return false;

  });

  this.$nav.on('click', 'button', function (e) { // close a tab
    // remove the link and tab content
    e.preventDefault();
    var target = $(this).attr('data-target');

    if (target != null) {
      target = target.substring(1); // remove #
      _this.remove(target);
    }
  });

  this.$tabContent = $('<div class="tab-content"></div>');
  this.$nav.on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) {
    // triggered when clicking tab
    var previous = _this.activeTabId;
    _this.activeTabId = $(e.target).data('link');
    _this.activeTabObject = _this.idToTabObject.get(_this.activeTabId);
    _this.$nav.find('li').removeClass('active');
    _this.$nav.find('[data-link=' + _this.activeTabId + ']').each(function () {
      $(this).parent().addClass('active');// not added via droptab
    });
    var scrollTop = document.body.scrollTop;
    $('#' + _this.activeTabId).focus();
    document.body.scrollTop = scrollTop; // focus can change scroll position
    if (_this.adding) {
      return;
    }
    _this.trigger('change', {
      tab: _this.activeTabId,
      previous: previous
    });
  });

};
phantasus.TabManager.prototype = {
  getTabText: function (id) {
    return this.$nav.find('> li > a').filter('a[data-link=' + id + ']').contents().first().text();
  },
  getTabCount: function () {
    return this.idToTabObject.size();
  },
  setTabText: function (id, text) {
    this.$nav.find('> li > a').filter('[data-link=' + id + ']').contents().first()
      .replaceWith(text + '&nbsp;');
    this.idToTabObject.get(id).setName(name);
  },
  /**
   * @param id
   *            Tab id
   * @param task
   * @param task.worker
   *            Optional worker that the task is run in.
   * @param task.name
   * @param task.tabId
   *            Tab id for task
   */
  addTask: function (task) {

  },
  removeTask: function (task) {

  },
  getWidth: function () {
    return this.$tabContent.outerWidth() || $(window).width();
  },
  getActiveTab: function () {
    return this.activeTabObject;
  },
  getActiveTabId: function () {
    return this.activeTabId;
  },

  /**
   *
   * @param options.object The object that stores the tab content state and has a setName if
   * function if rename is true.
   * @param options.$el
   *            the tab element
   * @param options.title
   *            the tab title
   * @param options.closeable
   *            Whether tab can be closed
   * @param options.rename
   *            Whether tab can be renamed
   * @param options.focus
   *            Whether new tab should be focused-note the change event is not
   *            triggered when true
   * @param options.enabled
   *            Whether new tab is enabled
   *
   */
  add: function (options) {
    this.adding = true;
    var id = _.uniqueId('phantasus-tab');
    this.idToTabObject.set(id, options.object);
    var li = [];
    li.push('<li class="phantasus-sortable" role="presentation">');
    li.push('<a data-phantasus-rename="' + options.rename
      + '" data-toggle="tab" data-link="' + id + '" href="#' + id + '">');
    li.push(options.title);
    li.push('&nbsp;<i style="color:black;"></i>');
    if (options.closeable) {
      li
        .push('&nbsp<button style="font-size: 18px;" type="button" class="close"' +
          ' aria-label="Close"' +
          ' data-target="#'
          + id
          + '"><span aria-hidden="true">×</span></button>');

    }
    li.push('</a></li>');
    var $link = $(li.join(''));
    $link.appendTo(this.$nav);
    var $panel = $('<div tabIndex="0" style="outline:0;cursor:default;" role="tabpanel"' +
      ' class="tab-pane" id="'
      + id + '"></div>');
    options.$el.appendTo($panel);
    $panel.appendTo(this.$tabContent);
    if (options.enabled === false) {
      $link.addClass('disabled');
      $link.find('a').addClass('btn disabled');
    }
    if (options.focus) {
      // update active tab, but don't fire event
      this.$nav.find('> li > a[data-toggle="tab"]:last').tab('show');
      this.activeTabId = id;
      this.activeTabObject = options.object;
      $panel.focus();
    }

    if (this.options.autohideTabBar) {
      this.$nav.css('display', this.idToTabObject.size() > 1 ? ''
        : 'none');
    }
    this.getTabCount() <= 1 ? this.$nav.sortable('disable') : this.$nav.sortable('enable');
    this.adding = false;
    this.trigger('add');
    return {
      $panel: $panel,
      id: id
    };
  },
  appendTo: function ($target) {
    this.$nav.appendTo($target);
    this.$tabContent.appendTo($target);
  },
  remove: function (target) {
    if (target === undefined) {
      target = this.activeTabId;
    }
    var obj = this.idToTabObject.remove(target);
    $('#' + target).remove(); // remove tab-pane
    this.activeTabObject = null;
    this._getA(target).parent().remove();
    this.$tabContent.find(target).remove();
    var $a = this.$nav.find('> li > a[data-toggle="tab"]:last');
    if ($a.length === 0) {
      // no content
      if (this.options.landingPage) {
        if (typeof this.options.landingPage === 'function') {
          this.options.landingPage().show();
        } else {
          this.options.landingPage.show();
        }
      }
    }

    $a.tab('show');
    if (this.options.autohideTabBar) {
      this.$nav.css('display', this.idToTabObject.size() > 1 ? ''
        : 'none');
    }
    this.getTabCount() <= 1 ? this.$nav.sortable('disable') : this.$nav.sortable('enable');
    if (this.idToTabObject.size() > 0) {
      $($a.attr('href')).focus();
    }
    if (obj != null && obj.onRemove) {
      obj.onRemove();
    }
    this.trigger('remove', {
      tab: target
    });
  },
  setOptions: function (options) {
    this.options = options;
    if (this.options.autohideTabBar) {
      this.$nav.css('display', this.idToTabObject.size() > 1 ? ''
        : 'none');
    }
  },
  getOptions: function () {
    return this.options;
  },
  setActiveTab: function (id) {
    if (id !== this.activeTabId) {
      var $a = this._getA(id);
      // make sure it's enabled
      $a.parent().removeClass('disabled');
      $a.removeClass('btn disabled');
      $a.tab('show');
      var previous = this.activeTabId;
      this.activeTabId = id;
      this.activeTabObject = this.idToTabObject.get(this.activeTabId);
      this.trigger('change', {
        tab: this.activeTabId,
        previous: previous
      });
    }

  },
  /**
   *
   * @param id
   *            The tab id
   * @param title
   *            The title (used to show tooltip)
   */
  setTabTitle: function (id, title) {
    this._getA(id).attr('title', title);
  },
  _getA: function (id) {
    if (id[0] === '#') {
      id = id.substring(1);
    }
    return this.$nav.find('> li > a[data-link=' + id + ']:first');
  },
  setTabEnabled: function (id, enabled) {
    var $a = this._getA(id);
    if (enabled) {
      $a.parent().removeClass('disabled');
      $a.removeClass('btn disabled');
    } else {
      $a.parent().addClass('disabled');
      $a.addClass('btn disabled');
    }

  },
  getIdToTabObject: function () {
    return this.idToTabObject;
  },
  getTabObject: function (id) {
    return this.idToTabObject.get(id);
  },
  rename: function (id) {
    var $a = this._getA(id);
    var $li = $a.parent();
    if ($li.hasClass('phantasus-tab-addon')) {
      return;
    }
    if (!$a.data('phantasus-rename') || !this.options.rename) {
      return;
    }
    var _this = this;
    var builder = new phantasus.FormBuilder();
    builder.append({
      name: 'name',
      type: 'text',
      value: $.trim($a.contents().first().text())
    });
    phantasus.FormBuilder.showOkCancel({
      title: 'Rename Tab',
      content: builder.$form,
      focus: document.activeElement,
      okCallback: function () {
        var name = $.trim(builder.getValue('name'));
        if (name !== '') {
          if (_this.activeTabObject != null && _this.activeTabObject.setName) {
            _this.activeTabObject.setName(name);
          }
          $a.contents().first().replaceWith(name + '&nbsp;');
          _this.trigger('rename');
        }
      }
    });
    // edit tab name
  }
};
phantasus.Util.extend(phantasus.TabManager, phantasus.Events);

/**
 * @param options.$el The jQuery element to render to. Must be in the DOM.
 * @param options.items An array of items to display in the table
 * @param options.search Whether to create a search widget
 * @param options.rowHeader Renderer to call for each row in the table
 * @param options.rowHeight Table row height
 * @param height: Height in pixels of table. '564px',
 * @param options.collapseBreakpoint: 500
 * @param options.showHeader: true
 * @param options.select: true
 * @param options.responsive: true
 * @param options.fixedWidth: Fixed table with when responsive is false. '320px'
 * @param options.columns An array of column descriptors. Each column can have the properties:
 * visible, name, field, renderer
 */



phantasus.Table = function (options) {
  options = phantasus.Table.createOptions(options);
  this.options = options;
  if (!options.width) {
    options.width = options.$el.attr('class');
  }
  var _this = this;

  var height = options.height;
  var $gridDiv = $('<div class="slick-table'
    + (options.tableClass ? (' ' + options.tableClass) : '')
    + '" style="width:' + options.fixedWidth + ';height:' + height
    + '"></div>');

  this.$gridDiv = $gridDiv;
  $gridDiv.appendTo(options.$el);
  // all columns (including those that are currently not visible */
  var columns = options.columns;
  this.columns = columns;
  var visibleColumns = columns.filter(function (c) {
    return c.visible;
  });
  var grid = new phantasus.Grid({
    gridOptions: {
      select: options.select,
      rowHeight: options.rowHeight,
      autoEdit: false,
      editable: false,
      autoHeight: options.height === 'auto',
      enableTextSelectionOnCells: true,
    },
    $el: $gridDiv,
    items: options.items,
    columns: visibleColumns
  });
  this.grid = grid;
  this.searchFunction = null;
  var searchFilter = {
    isEmpty: function () {
      return _this.searchFunction == null;
    },
    init: function () {
    },
    accept: function (item) {
      return _this.searchFunction(item);
    }
  };
  // add empty search filter
  this.grid
    .getFilter().add(searchFilter);
  var $header = $('<div class="slick-table-header"><div name="top"></div><div style="display: inline-block;" name="left" class="pad-bottom-8 pad-top-8"></div><div name="right" class="pull-right pad-bottom-8' +
    ' pad-top-8"></div></div>');
  this.$header = $header;
  var $right = $header.find('.pull-right');
  if (options.search) {
    var tableSearch = new phantasus.TableSearchUI({
      $el: $header.find('[name=top]'),
      $right: $right
    });
    tableSearch.setTable(this);
    this.tableSearch = tableSearch;
  }
  if (options.columnPicker && visibleColumns.length !== this.columns.length) {
    var select = [];
    select
      .push('<select data-width="90px" data-selected-text-format="static" title="Columns..." multiple class="pad-left-4 selectpicker show-tick">');
    // sort column names
    var sortedColumns = this.columns.slice().sort(function (a, b) {
      a = a.name.toLowerCase();
      b = b.name.toLowerCase();
      return (a === b ? 0 : (a < b ? -1 : 1));
    });
    sortedColumns.forEach(function (c, i) {
      select.push('<option value="' + i + '"');
      if (c.visible) {
        select.push(' selected');
      }
      select.push('>');
      select.push(c.name);
      select.push('</option>');
    });
    select.push('</select>');
    var $select = $(select.join(''));
    $select.appendTo($right);
    $select.selectpicker({
      iconBase: 'fa',
      tickIcon: 'fa-check',
      style: 'btn-default btn-xs'
    });
    $select.on('change', function () {
      var oldColumns = grid.getColumns().map(function (c) {
        return c.id;
      });
      var selectedColumnIndices = $select.val();
      visibleColumns = [];
      for (var i = 0; i < selectedColumnIndices.length; i++) {
        visibleColumns.push(sortedColumns[parseInt(selectedColumnIndices[i])]);
      }
      var newColumns = visibleColumns.map(function (c) {
        return c.id;
      });

      grid.setColumns(visibleColumns);

      if (newColumns.length > oldColumns.length) {
        var set = new phantasus.Set();
        for (var i = 0; i < newColumns.length; i++) {
          set.add(newColumns[i]);
        }
        for (var i = 0; i < oldColumns.length; i++) {
          set.remove(oldColumns[i]);
        }
        var added = set.values();

        grid.setSortColumns([{
          columnId: added[0],
          sortAsc: true
        }]);
      }
      // if column added, sort by added column
      _this.resize();
      _this.redraw();

    });
  }
  $header.prependTo(options.$el);
  var collapsed = false;
  var lastWidth = -1;
  var resize = function () {
    if (!_this.options.responsive) {
      return;
    }

    var gridWidth = options.$el.width();
    if (gridWidth === lastWidth) {
      return;
    }
    lastWidth = gridWidth;

    $gridDiv.css('width', gridWidth + 'px');
    // if (options.responsiveHeight) {
    // var verticalPosition = _this.$gridDiv[0].getBoundingClientRect().top
    // + window.pageYOffset;
    // $gridDiv.css('height',
    // (document.body.clientHeight - verticalPosition) + 'px');
    // }
    if (!collapsed && gridWidth < options.collapseBreakpoint
      && visibleColumns.length > 1) {
      collapsed = true;
      $gridDiv.addClass('slick-stacked');

      _this.grid.grid.getOptions().rowHeight = (options.collapsedRowHeight ? options.collapsedRowHeight : options.rowHeight)
        * visibleColumns.length;
      // collapse
      _this.grid.grid
        .setColumns([{
          id: 0,
          tooltip: function (item, value) {
            var html = [];
            for (var i = 0; i < visibleColumns.length; i++) {
              var text = visibleColumns[i].tooltip(item, visibleColumns[i]
                .getter(item));
              if (text != null && text !== '') {
                html.push(text);
              }
            }
            return html.join('<br />');
          },
          collapsed: true,
          getter: function (item) {
            return item;
          },
          formatter: function (row, cell, value, columnDef,
                               dataContext) {
            var html = [];
            html
              .push('<div class="slick-table-wrapper"><div class="slick-cell-wrapper">');
            if (options.rowHeader) { // e.g. render checkbox
              html.push(options.rowHeader(dataContext));
              html.push('<div style="height:4px;"></div>');
            }
            for (var i = 0; i < visibleColumns.length; i++) {
              if (i > 0) {
                html.push('<div style="height:4px;"></div>');
              }
              var c = visibleColumns[i];
              html.push(c.name);
              html.push(':');
              var s = c.renderer(dataContext, c
                .getter(dataContext));
              html.push(s);

            }
            html.push('</div></div>');
            return html.join('');
          },
          sortable: false,
          name: ''
        }]);
      $gridDiv.find('.slick-header').hide();
      _this.grid.grid.resizeCanvas();
      _this.grid.grid.invalidate();

    } else if (collapsed && gridWidth >= options.collapseBreakpoint) {
      $gridDiv.removeClass('slick-stacked');
      collapsed = false;
      if (options.showHeader) {
        $gridDiv.find('.slick-header').show();
      }
      _this.grid.grid.getOptions().rowHeight = options.rowHeight;
      _this.grid.grid.setColumns(visibleColumns);
      _this.grid.grid.resizeCanvas();
      if (options.select) {
        _this.grid.grid.setSelectedRows(_this.grid.grid
          .getSelectedRows());
      }
      _this.grid.grid.invalidate();
    } else {
      _this.grid.grid.resizeCanvas();
      _this.grid.grid.invalidate();
    }
    _this.grid.maybeAutoResizeColumns();

  };
  if (!options.showHeader) {
    $gridDiv.find('.slick-header').hide();
  }
  if (options.responsive) {
    $(window).on('resize orientationchange', resize);
    $gridDiv.on('remove', function () {
      $(window).off('resize', resize);
    });
    resize();
  }
  this.resize = resize;
  if (visibleColumns.length > 1 && options.items != null
    && options.items.length > 0) {
    this.setItems(options.items);
  }
  if (!$gridDiv.is(':visible')) {
    // find 1st parent that is not visible
    var $parent = $gridDiv;
    var observer = new MutationObserver(function (mutations) {
      if (window.getComputedStyle($parent[0]).display !== 'none') {
        observer.disconnect();
        resize();
      }
    });

    while ($parent.length > 0) {
      if (window.getComputedStyle($parent[0]).display === 'none') {
        break;
      }
      $parent = $parent.parent();

    }

    if ($parent.length > 0) {
      observer.observe($parent[0], {
        attributes: true,
        childList: false,
        characterData: false
      });
    }

  }
}
;

phantasus.Table.defaultRenderer = function (item, value) {
  if (value == null) {
    return '';
  } else if (_.isNumber(value)) {
    return phantasus.Util.nf(value);
  } else if (phantasus.Util.isArray(value)) {
    var s = [];
    for (var i = 0, length = value.length; i < length; i++) {
      if (i > 0) {
        s.push(', ');
      }
      var val = value[i];
      s.push(value[i]);
    }
    return s.join('');
  } else {
    return '' + value;
  }
};

phantasus.Table.prototype = {
  toText: function () {
    var text = [];
    var items = this.getItems();
    var columns = this.columns.filter(function (c) {
      return c.visible;
    });
    for (var j = 0; j < columns.length; j++) {
      if (j > 0) {
        text.push('\t');
      }
      text.push(columns[j].name);
    }
    text.push('\n');
    for (var i = 0; i < items.length; i++) {
      var item = items[i];
      for (var j = 0; j < columns.length; j++) {
        if (j > 0) {
          text.push('\t');
        }
        var value = columns[j].getter(item);
        text.push(phantasus.Util.toString(value));
      }
      text.push('\n');
    }
    return text.join('');
  },
  setHeight: function (height) {
    this.options.height = height;
    if (height === 'auto') {
      this.$gridDiv.css('height', '');
      this.grid.grid.getOptions().autoHeight = true;
      this.grid.grid.setOptions(this.grid.grid.getOptions());

    } else {
      this.$gridDiv.css('height', height);
    }
    this.grid.grid.resizeCanvas();
    if (height === 'auto') {
      var height = this.getItems().length * this.options.rowHeight
        + this.options.rowHeight;
      this.$gridDiv.find('.slick-viewport').css('height', height + 'px');
    }
    this.grid.grid.invalidate();

  },
  setSearchVisible: function (visible) {
    this.$header.find('[name=search]').css('display', visible ? '' : 'none');
  },
  autocomplete: function (tokens, response) {
    var matches = [];
    var token = tokens != null && tokens.length > 0 ? tokens[tokens.selectionStartIndex]
      : '';
    token = $.trim(token);
    var columns = this.columns.filter(function (c) {
      return (c.searchable && c.visible) || c.alwaysSearch;
    });

    var ncolumns = columns.length;
    var showField = ncolumns > 1;
    if (token === '') {
      if (ncolumns <= 1) {
        return response(matches);
      }
      for (var i = 0; i < ncolumns; i++) {
        var field = columns[i].name;
        matches.push({
          value: field + ':',
          label: '<span style="font-weight:300;">' + field
          + ':</span>',
          show: true
        });
        // show column names

      }
      matches
        .sort(function (a, b) {
          return (a.value === b.value ? 0
            : (a.value < b.value ? -1 : 1));
        });
      return response(matches);
    }
    var field = null;
    var semi = token.indexOf(':');
    var regex = new RegExp('^' + phantasus.Util.escapeRegex(token), 'i');
    if (semi > 0) { // field search?
      if (token.charCodeAt(semi - 1) !== 92) { // \:
        var possibleField = $.trim(token.substring(0, semi));
        if (possibleField.length > 0 && possibleField[0] === '"'
          && possibleField[token.length - 1] === '"') {
          possibleField = possibleField.substring(1,
            possibleField.length - 1);
        }
        var columnNameToColumn = new phantasus.Map();
        var columnNames = columns.map(function (c) {
          return c.name;
        });
        for (var i = 0; i < columnNames.length; i++) {
          columnNameToColumn.set(columnNames[i], columns[i]);
        }
        var c = columnNameToColumn.get(possibleField);
        if (c !== undefined) {
          token = $.trim(token.substring(semi + 1));
          columns = [c];
          ncolumns = 1;
        }
      }

    } else if (ncolumns > 1) {
      var regex = new RegExp('^' + phantasus.Util.escapeRegex(token), 'i');
      for (var j = 0; j < ncolumns; j++) {
        var field = columns[j].name;
        if (regex.test(field)) {
          matches.push({
            value: field + ':',
            label: '<span style="font-weight:300;">' + field
            + ':</span>',
            show: true
          });
        }
      }
    }
    var set = new phantasus.Set();
    var regex = new RegExp('^' + phantasus.Util.escapeRegex(token), 'i');
    var items = this.getItems();
    var dataTypes = [];
    // filter numeric columns
    var filteredColumns = [];
    columns.forEach(function (c) {
      var dataType = null;
      for (var i = 0, nitems = items.length; i < nitems; i++) {
        var value = c.getter(items[i]);
        if (value != null) {
          dataType = phantasus.Util.getDataType(value);
          break;
        }
      }
      if (dataType === 'string' || dataType === '[string]') {
        dataTypes.push(dataType);
        filteredColumns.push(c);
      }
    });
    columns = filteredColumns;
    ncolumns = columns.length;
    var maxSize = matches.length + 10;
    for (var i = 0, nitems = items.length; i < nitems; i++) {
      var item = items[i];
      for (var j = 0; j < ncolumns; j++) {
        var field = columns[j].name;
        var value = columns[j].getter(item);
        var dataType = dataTypes[j];
        if (dataType === '[string]') {
          var nvalues = value == null ? 0 : value.length;
          for (var k = 0; k < nvalues; k++) {
            var val = value[k];
            if (regex.test(val) && !set.has(val)) {
              set.add(val);
              matches
                .push({
                  value: showField ? (field + ':' + val)
                    : val,
                  label: showField ? ('<span style="font-weight:300;">'
                  + field
                  + ':</span>'
                  + '<span style="font-weight:900;">'
                  + val + '</span>')
                    : ('<span style="font-weight:900;">'
                  + val + '</span>')
                });
            }
            if (matches.length === maxSize) {
              return response(matches);
            }
          }
        } else {
          if (regex.test(value) && !set.has(value)) {
            set.add(value);
            matches
              .push({
                value: showField ? (field + ':' + value)
                  : value,
                label: showField ? ('<span style="font-weight:300;">'
                + field
                + ':</span>'
                + '<span style="font-weight:900;">'
                + value + '</span>')
                  : ('<span style="font-weight:900;">'
                + value + '</span>')
              });
            if (matches.length === maxSize) {
              return response(matches);
            }
          }
        }

      }
    }
    return response(matches);

  },
  searchWithPredicates: function (predicates) {
    if (predicates == null || predicates.length === 0) {
      this.searchFunction = null;
      this.grid
        .setFilter(this.grid
          .getFilter());
      return;
    }
    var columns = this.columns.filter(function (c) {
      return (c.searchable && c.visible) || c.alwaysSearch;
    });
    var columnNameToColumn = new phantasus.Map();
    var columnNames = columns.map(function (c) {
      return c.name;
    });
    for (var i = 0; i < columnNames.length; i++) {
      columnNameToColumn.set(columnNames[i], columns[i]);
    }

    var filteredPredicates = [];
    var npredicates = predicates.length;
    for (var i = 0; i < npredicates; i++) {
      var predicate = predicates[i];
      var filterColumnName = predicate.getField();
      if (filterColumnName != null) {
        var column = columnNameToColumn.get(filterColumnName);
        if (column) {
          predicate.column = column;
          filteredPredicates.push(predicate);
        }
      } else {
        filteredPredicates.push(predicate);
      }
    }
    predicates = filteredPredicates;
    npredicates = predicates.length;
    var f = function (item) {
      for (var p = 0; p < npredicates; p++) {
        var predicate = predicates[p];
        var searchColumns;
        if (predicate.column) {
          searchColumns = [predicate.column];
        } else {
          searchColumns = columns;
        }
        for (var j = 0, ncolumns = searchColumns.length; j < ncolumns; j++) {
          var value = searchColumns[j].getter(item);
          if (phantasus.Util.isArray(value)) {
            var nvalues = value.length;
            for (var i = 0; i < nvalues; i++) {
              if (predicate.accept(value[i])) {
                return true;
              }
            }
          } else {
            var predicate = predicates[p];
            if (predicate.accept(value)) {
              return true;
            }
          }
        }

      }

      return false;
    };
    this.searchFunction = f;
    this.grid
      .setFilter(this.grid
        .getFilter());
  },
  search: function (text) {
    if (text === '') {
      this.searchFunction = null;
      this.grid
        .setFilter(this.grid
          .getFilter());
    } else {
      var tokens = phantasus.Util.getAutocompleteTokens(text);
      var columns = this.columns.filter(function (c) {
        return (c.searchable && c.visible) || c.alwaysSearch;
      });
      var columnNames = columns.map(function (c) {
        return c.name;
      });
      var predicates = phantasus.Util.createSearchPredicates({
        tokens: tokens,
        fields: columnNames
      });
      this.searchWithPredicates(predicates);
    }
  },
  getSelectedRows: function () {
    return this.grid.getSelectedRows();
  },
  getSelectedItems: function () {
    return this.grid.getSelectedItems();
  },
  getSelectedItem: function () {
    return this.grid.getSelectedItem();
  },
  setSelectedRows: function (rows) {
    this.grid.setSelectedRows(rows);
  },
  getItems: function (items) {
    return this.grid.getItems();
  },
  getAllItemCount: function () {
    return this.grid.getAllItemCount();
  },
  getFilteredItemCount: function () {
    return this.grid.getFilteredItemCount();
  },
  setFilter: function (f) {
    this.grid.setFilter(f);
  },
  getFilter: function () {
    return this.grid.getFilter();
  },
  setItems: function (items) {
    this.grid.setItems(items);
    this.grid.redraw();
    // TODO update height?
  },
  redraw: function () {
    this.grid.redraw();
  },
  /**
   * @param evtStr
   *            selectionChanged
   */
  on: function (evtStr, handler) {
    this.grid.on(evtStr, handler);
    return this;
  },
  off: function (evtStr, handler) {
    this.grid.off(evtStr, handler);
    return this;
  },
  trigger: function (evtStr) {
    this.grid.trigger(evtStr);
  }
};

phantasus.Table.createOptions = function (options) {
  options = $.extend(true, {}, {
    items: [],
    height: '564px',
    collapseBreakpoint: 400,
    showHeader: true,
    select: true,
    rowHeader: null,
    responsive: true,
    fixedWidth: '320px',
    columnPicker: true
  }, options);

  if (!options.columns) {
    options.columns = [{
      name: ''
    }];
  }
  var columns = [];
  options.columns.forEach(function (c, i) {
    var column = $.extend(true, {}, {
      id: i,
      tooltip: function (dataContext, value) {
        return phantasus.Table.defaultRenderer(dataContext, value);
      },
      formatter: function (row, cell, value, columnDef, dataContext) {

        var html = [];
        html.push('<div class="slick-table-wrapper"><div class="slick-cell-wrapper">');
        if (options.rowHeader && cell === 0) {
          html.push(options.rowHeader(dataContext));
        }
        html.push(column.renderer(dataContext, value));
        html.push('</div></div>');
        return html.join('');

      },
      comparator: function (a, b) {
        var aNaN = (a == null || _.isNumber(a) && isNaN(a));
        var bNaN = (b == null || _.isNumber(b) && isNaN(b));
        if (aNaN && bNaN) {
          return 0;
        }
        if (aNaN) {
          return 1;
        }
        if (bNaN) {
          return -1;
        }
        if (a.toLowerCase) {
          a = a.toLowerCase();
        }
        if (b.toLowerCase) {
          b = b.toLowerCase();
        }

        return (a === b ? 0 : (a < b ? -1 : 1));
      },
      sortable: true,
      searchable: true,
      width: null,
      name: c.name,
      renderer: phantasus.Table.defaultRenderer
    }, c);

    if (column.visible === undefined) {
      column.visible = true;
    }
    if (!column.getter) {
      column.getter = column.field == null ? function (item) {
        return item;
      } : function (item) {
        return item[c.field];
      };
    }

    columns.push(column);
  });

  options.columns = columns;
  if (options.columns.length === 1) {
    options.tableClass = 'slick-table-compact';
  } else {
    options.tableClass = 'slick-bordered-table';
  }
  if (!options.rowHeight) {
    // options.rowHeight = options.tableClass === 'slick-table-compact' ? 18
    // 	: 20;
    options.rowHeight = 22;
  }
  return options;
};

phantasus.TableSearchUI = function (options) {
  var _this = this;
  var $search = $('<input name="search" type="text" class="form-control input-sm"' +
    ' placeholder="Search" autocomplete="off">');
  $search.appendTo(options.$el);
  this.$search = $search;
  this.$searchResults = $('<span class="pad-top-2 tableview-rowcount" name="search"></span>');
  this.$showAll = $('<div style="display:inline-block;min-width:60px;" name="search" class="pad-left-8 text-button-copy tableview-rowcount">Show' +
    ' all</div>');
  this.$searchResults.appendTo(options.$right);
  this.$showAll.appendTo(options.$right);
  this.$showAll.on('click', function (e) {
    e.preventDefault();
    $search.val('');
    _this.table.search('');
    _this.table.trigger('showAll', {table: _this.table});

  });
  $search.on('keyup', _.debounce(function () {
    _this.table.search($.trim($(this).val()));
  }, 100));
  phantasus.Util.autosuggest({
    $el: $search,
    suggestWhenEmpty: true,
    filter: function (tokens, response) {
      _this.table.autocomplete(tokens, response);
    },
    select: function () {
      _this.table.search($.trim($search.val()));
    }
  });
};

phantasus.TableSearchUI.prototype = {
  updateSearchLabel: function () {
    var text = 'Showing: ' + phantasus.Util.intFormat(this.table.getFilteredItemCount()) + ' / ' + phantasus.Util.intFormat(this.table.getAllItemCount());
    this.$searchResults.html(text);
  },
  setTable: function (table) {
    this.table = table;
    var _this = this;

    table.on('filter', function () {
      _this.updateSearchLabel();
    });

  }

};


phantasus.TrackSelection = function (track, positions, selectionModel, isColumns,
                                    heatMap) {
  var canvas = track.canvas;
  var startIndex = -1;
  var coord = isColumns ? 'x' : 'y';

  function getPosition(event, useDelta) {
    if (track.settings.squished) {
      var total = positions.getPosition(positions.getLength() - 1)
        + positions.getItemSize(positions.getLength() - 1);
      var squishFactor = total
        / (isColumns ? track.getUnscaledWidth() : track
          .getUnscaledHeight());
      var clientXY = phantasus.CanvasUtil.getClientXY(event, useDelta);
      var p = phantasus.CanvasUtil.getMousePosWithScroll(event.target,
        event, 0, 0, useDelta);
      p[coord] *= squishFactor;
      return p;

    } else {
      return phantasus.CanvasUtil.getMousePosWithScroll(event.target,
        event, heatMap.scrollLeft(), heatMap.scrollTop(),
        useDelta);
    }

  }

  var panning = false;
  var scrollIntervalId;
  var lastScrollTime = new Date().getTime();
  var _this = this;
  var throttlePanMove = 50;

  function mouseleave(e) {

    // listen for mouse hold events
    var scroll = function () {
      var now = new Date().getTime();
      var rect = canvas.getBoundingClientRect();
      var doPan = false;
      if (!isColumns) {
        if (e.clientY > rect.bottom || e.clientY < rect.top) {
          doPan = true;
        }
      } else {
        if (e.clientX > rect.right || e.clientX < rect.left) {
          doPan = true;
        }
      }
      if (doPan) {
        _this.panmove(e);
        scrollIntervalId = setTimeout(scroll, throttlePanMove);
      }
    };
    scrollIntervalId = setTimeout(scroll, throttlePanMove);
    $(canvas).one('mouseover', mouseover);
  }

  function mouseover() {
    // on mouse exit, see if mouse held
    // on mouse enter, stop listening
    clearTimeout(scrollIntervalId);
    $(canvas).one('mouseleave', mouseleave);
  }

  this.hammer = phantasus.Util
    .hammer(canvas, ['pan', 'tap', 'longpress'])
    .on('longpress', this.longpress = function (event) {
      event.preventDefault();
      event.srcEvent.stopImmediatePropagation();
      event.srcEvent.stopPropagation();
      heatMap.setSelectedTrack(track.name, isColumns);
      track.showPopup(event.srcEvent);
    }).on('panend', this.panend = function (event) {
      panning = false;
      clearInterval(scrollIntervalId);
      $(canvas).off('mouseover mouseleave');
    })
    .on(
      'panmove',
      this.panmove = function (event) {
        var now = new Date().getTime();
        var elapsed = now - lastScrollTime;
        if (elapsed < throttlePanMove) {
          return;
        }

        var position = getPosition(event);
        var endIndex = positions.getIndex(position[coord],
          false);
        var commandKey = (event.srcEvent == null) ? false : (phantasus.Util.IS_MAC ? event.srcEvent.metaKey
          : event.srcEvent.ctrlKey);
        var viewIndices = commandKey ? selectionModel
          .getViewIndices() : new phantasus.Set();
        var _startIndex = startIndex;
        if (startIndex > endIndex) {
          var tmp = endIndex;
          endIndex = _startIndex;
          _startIndex = tmp;
        }
        for (var i = _startIndex; i <= endIndex; i++) {
          viewIndices.add(i);
        }
        selectionModel.setViewIndices(viewIndices, true);
        if (!isColumns) {
          var scrollTop = heatMap.scrollTop();
          var heatMapHeight = heatMap.heatmap.getUnscaledHeight();
          var scrollBottom = scrollTop + heatMapHeight;
          if (position.y > scrollBottom) { // scroll down
            heatMap.scrollTop(position.y + 8 - heatMapHeight);
          } else if (position.y < scrollTop) {
            heatMap.scrollTop(position.y - 8);
          }
        } else {
          var scrollLeft = heatMap.scrollLeft();
          var heatMapWidth = heatMap.heatmap.getUnscaledWidth();
          var scrollRight = scrollLeft + heatMapWidth;
          if (position.x > scrollRight) {
            heatMap.scrollLeft(position.x + 8 - heatMapWidth);
          } else if (position.x < scrollLeft) {
            heatMap.scrollLeft(position.x - 8);
          }
        }
        event.preventDefault();
        if (event.srcEvent != null) {
          event.srcEvent.stopPropagation();
          event.srcEvent.stopImmediatePropagation();
        }
        lastScrollTime = new Date().getTime();
      })
    .on('panstart', this.panstart = function (event) {
      heatMap.setSelectedTrack(track.name, isColumns);
      var position = getPosition(event, true);
      startIndex = positions.getIndex(position[coord], false);
      panning = true;

      $(canvas).one('mouseleave.phantasus', mouseleave);

    })
    .on(
      'tap doubletap',
      this.tap = function (event) {
        var position = getPosition(event);
        var index = positions.getIndex(position[coord], false);
        if (event.tapCount > 1) {
          if ((isColumns && !heatMap.options.columnsSortable)
            || (!isColumns && !heatMap.options.rowsSortable)) {
            return;
          }
          heatMap.sortBasedOnSelection(null, isColumns,
            event.srcEvent.shiftKey);
        } else {
          heatMap.setSelectedTrack(track.name, isColumns);
          var commandKey = phantasus.Util.IS_MAC ? event.srcEvent.metaKey
            : event.srcEvent.ctrlKey;
          if (phantasus.Util.IS_MAC && event.srcEvent.ctrlKey) { // right-click
            // on
            // Mac
            return;
          }
          var viewIndices;
          if (commandKey) { // toggle selection
            viewIndices = selectionModel.getViewIndices();
            if (viewIndices.has(index)) {
              viewIndices.remove(index);
            } else {
              viewIndices.add(index);
            }
          } else if (event.srcEvent.shiftKey) { // add to
            // selection
            viewIndices = selectionModel.getViewIndices();
            var min = Number.MAX_VALUE;
            var max = -Number.MAX_VALUE;
            viewIndices.forEach(function (viewIndex) {
              min = Math.min(viewIndex, min);
              max = Math.max(viewIndex, max);
            });

            if (index >= max) { // select from index to max
              for (var i = max; i <= index; i++) {
                viewIndices.add(i);
              }
            } else {// select from index to min
              for (var i = Math.min(index, min), max = Math
                .max(index, min); i <= max; i++) {
                viewIndices.add(i);
              }
            }
          } else {
            viewIndices = new phantasus.Set();
            viewIndices.add(index);
          }
          selectionModel.setViewIndices(viewIndices, true);
        }
      });
};
phantasus.TrackSelection.prototype = {
  dispose: function () {
    this.hammer.off('longpress', this.longpress).off('panstart',
      this.panstart).off('panmove', this.panmove).off('panend', this.panend).off('tap', this.tap).off('doubletap', this.tap);
    this.hammer.destroy();
  }
};

phantasus.VectorTrackHeader = function (project, name, isColumns, heatMap) {
  phantasus.AbstractCanvas.call(this);
  this.font = {weight: '400'};
  this.project = project;
  this.name = name;
  this.isColumns = isColumns;
  var canvas = this.canvas;
  this.heatMap = heatMap;
  this.selectedBackgroundColor = '#c8c8c8';
  this.backgroundColor = 'rgb(255,255,255)';
  var vector = (isColumns ? project.getFullDataset().getColumnMetadata()
    : project.getFullDataset().getRowMetadata()).getByName(name);
  if (vector && vector.getProperties().has(phantasus.VectorKeys.TITLE)) {
    this.canvas.setAttribute('title', vector.getProperties().get(
      phantasus.VectorKeys.TITLE));
    $(this.canvas).tooltip();
  }

  var _this = this;

  this.setBounds({
    height: this.defaultFontHeight
    + phantasus.VectorTrackHeader.FONT_OFFSET
  });

  function getResizeCursor(pos) {
    if (isColumns) {
      if (pos.y < 3) {
        return {
          cursor: 'ns-resize',
          isPrevious: true
        };
      }
      if (pos.y >= (_this.getUnscaledHeight() - 3)) {
        return {
          cursor: 'ns-resize',
          isPrevious: false
        };
      }
      if (pos.x >= (_this.getUnscaledWidth() - 3)) { // change change column width
        return {
          isPrevious: false,
          cursor: 'ew-resize'
        };
      }
    } else {
      if (pos.x < 3 && heatMap.getTrackIndex(name, isColumns) > 0) { // can't drag left on 1st
        // row header
        return {
          cursor: 'ew-resize',
          isPrevious: true
        };
      }
      if (pos.x >= (_this.getUnscaledWidth() - 3)) {
        return {
          cursor: 'ew-resize',
          isPrevious: false
        };
      }
    }
  }

  var mouseMove = function (event) {
    if (!phantasus.CanvasUtil.dragging) {
      var pos = phantasus.CanvasUtil.getMousePos(event.target, event);
      var resizeCursor = getResizeCursor(pos);
      canvas.style.cursor = resizeCursor == null ? 'default' : resizeCursor.cursor;
      //document.body.style.cursor = !cursor ? 'default' : cursor;
      _this.isMouseOver = true;
      _this.repaint();
    }

  };
  var mouseExit = function (e) {
    if (!phantasus.CanvasUtil.dragging) {
      canvas.style.cursor = 'default';
    }
    _this.isMouseOver = false;
    _this.repaint();
  };
  var showPopup = function (e) {
    heatMap.setSelectedTrack(_this.name, isColumns);
    var track = heatMap.getTrack(_this.name, isColumns);
    if (!track) {
      throw _this.name + ' track not found';
    }
    e.preventDefault();
    if (e.stopPropagation) {
      e.stopPropagation();
    }
    if (e.stopImmediatePropagation) {
      e.stopImmediatePropagation();
    }
    track.showPopup(e, true);
    return false;
  };

  $(this.canvas).css({'background-color': this.backgroundColor}).on(
    'mousemove.phantasus', mouseMove).on('mouseout.phantasus', mouseExit)
    .on('mouseenter.phantasus', mouseMove).on('contextmenu.phantasus', showPopup).addClass('phantasus-track-header ' + (isColumns ? 'phantasus-columns' : 'phantasus-rows'));

  var resizeCursor;
  var dragStartWidth = 0;
  var dragStartHeight = 0;
  var reorderingTrack = false;
  var dragStartPosition;
  var resizeTrackName;
  // var throttled = _.throttle(function(event) {
  //
  // if (event.type === 'mouseout') {
  // } else {
  // }
  // }, 100);
  // $(canvas).on('mouseout', throttled).on('mousemove', throttled);
  this.hammer = phantasus.Util
  .hammer(canvas, ['pan', 'tap', 'longpress'])
  .on('longpress', this.longpress = function (event) {
    event.preventDefault();
    heatMap.setSelectedTrack(_this.name, isColumns);
    var track = heatMap.getTrack(_this.name, isColumns);
    track.showPopup(event.srcEvent, true);
  })
  .on(
    'panend',
    this.panend = function (event) {
      _this.isMouseOver = false;
      phantasus.CanvasUtil.dragging = false;
      canvas.style.cursor = 'default';
      var index = heatMap.getTrackIndex(_this.name,
        isColumns);
      var header = heatMap.getTrackHeaderByIndex(index,
        isColumns);
      var track = heatMap
      .getTrackByIndex(index, isColumns);
      var $canvas = $(track.canvas);
      $canvas.css('z-index', '0');
      $(header.canvas).css('z-index', '0');
      heatMap.revalidate();
    })
  .on(
    'panstart',
    this.panstart = function (event) {
      _this.isMouseOver = false;

      if (phantasus.CanvasUtil.dragging) {
        return;
      }
      if (resizeCursor != null) { // resize
        phantasus.CanvasUtil.dragging = true;
        canvas.style.cursor = resizeCursor.cursor;
        if (resizeCursor.isPrevious) {
          var index = heatMap.getTrackIndex(_this.name,
            isColumns);
          index--; // FIXME index = -1
          if (index === -1) {
            index = 0;
          }
          var header = heatMap.getTrackHeaderByIndex(
            index, isColumns);
          dragStartWidth = header.getUnscaledWidth();
          dragStartHeight = header.getUnscaledHeight();
          resizeTrackName = header.name;
        } else {
          resizeTrackName = null;
          dragStartWidth = _this.getUnscaledWidth();
          dragStartHeight = _this.getUnscaledHeight();
        }
        event.preventDefault();
        reorderingTrack = false;
      } else { // move track
        var index = heatMap.getTrackIndex(_this.name,
          isColumns);
        if (index == -1) {
          throw _this.name + ' not found';
        }
        var header = heatMap.getTrackHeaderByIndex(
          index, isColumns);
        var track = heatMap.getTrackByIndex(index,
          isColumns);
        heatMap.setSelectedTrack(_this.name, isColumns);
        var $canvas = $(track.canvas);
        dragStartPosition = $canvas.position();
        $canvas.css('z-index', '100');
        $(header.canvas).css('z-index', '100');
        phantasus.CanvasUtil.dragging = true;
        resizeCursor = undefined;
        reorderingTrack = true;
      }
    })
  .on(
    'panmove',
    this.panmove = function (event) {
      _this.isMouseOver = false;
      if (resizeCursor != null) {
        var width;
        var height;
        if (resizeCursor.cursor === 'ew-resize') {
          var dx = event.deltaX;
          width = Math.max(8, dragStartWidth + dx);
        }

          if (resizeCursor.cursor === 'ns-resize') {
            var dy = event.deltaY;
            height = Math.max(8, dragStartHeight + dy);
          }

          heatMap.resizeTrack(resizeTrackName == null ? _this.name : resizeTrackName, width, height,
            isColumns);
        } else if (reorderingTrack) { // reorder
          var index = heatMap.getTrackIndex(_this.name,
            isColumns);
          var header = heatMap.getTrackHeaderByIndex(
            index, isColumns);
          var track = heatMap.getTrackByIndex(index,
            isColumns);
          var ntracks = heatMap.getNumTracks(isColumns);
          var delta = isColumns ? event.deltaY : event.deltaX;
          var newIndex = index + (delta > 0 ? 1 : -1);
          newIndex = Math.min(Math.max(0, newIndex),
            ntracks - 1);
          var prop = isColumns ? 'top' : 'left';
          var w = isColumns ? 'getUnscaledHeight'
            : 'getUnscaledWidth';
          var trackBounds = {};
          trackBounds[prop] = dragStartPosition[prop] + delta;
          track.setBounds(trackBounds);
          header.setBounds(trackBounds);
          var dragOverTrack = heatMap.getTrackByIndex(
            newIndex, isColumns);
          var dragOverWidth = dragOverTrack[w]();
          var dragOverLeft = $(dragOverTrack.canvas)
            .position()[prop];
          var dragleft = dragStartPosition[prop] + delta;
          var dragright = dragleft + track[w]();
          if ((delta > 0 && dragright >= dragOverLeft
            + dragOverWidth / 2)
            || (delta < 0 && dragleft <= dragOverLeft
            + dragOverWidth / 2)) {
            if (index !== newIndex) {
              heatMap.moveTrack(index, newIndex,
                isColumns);
              var otherHeader = heatMap
                .getTrackHeaderByIndex(index,
                  isColumns);
              var otherTrack = heatMap
                .getTrackByIndex(index, isColumns);
              var $movedCanvas = $(otherTrack.canvas);
              var newLeft = $movedCanvas.position()[prop];
              if (delta < 0) {
                newLeft += track[w]();
              } else {
                newLeft -= track[w]();
              }
              var otherBounds = {};
              otherBounds[prop] = newLeft;
              otherTrack.setBounds(otherBounds);
              otherHeader.setBounds(otherBounds);
            }
          }
        }
      })
    .on(
      'tap',
      this.tap = function (event) {
        if (phantasus.Util.IS_MAC && event.srcEvent.ctrlKey) { // right-click
          return;
        }
        _this.isMouseOver = false;
        heatMap.setSelectedTrack(_this.name, isColumns);
        var vector = (isColumns ? project.getFullDataset().getColumnMetadata()
          : project.getFullDataset().getRowMetadata()).getByName(name);
        // vector will be null for row #
        if ((isColumns && !heatMap.options.columnsSortable) || vector == null) {
          return;
        }
        if ((!isColumns && !heatMap.options.rowsSortable) || vector == null) {
          return;
        }

        var additionalSort = event.srcEvent.shiftKey;
        var isGroupBy = false; // event.srcEvent.altKey;

        var existingSortKeyIndex = _this
          .getSortKeyIndexForColumnName(_this
            .getSortKeys(), _this.name);
        var sortOrder;
        var sortKey;

        var dataType = phantasus.VectorUtil.getDataType(vector);
        if (existingSortKeyIndex != null) {
          sortKey = _this.getSortKeys()[existingSortKeyIndex.index];
          if (sortKey.getSortOrder() === phantasus.SortKey.SortOrder.UNSORTED) {
            sortOrder = phantasus.SortKey.SortOrder.ASCENDING; // 1st
            // click
          } else if (sortKey.getSortOrder() === phantasus.SortKey.SortOrder.ASCENDING) {
            sortOrder = phantasus.SortKey.SortOrder.DESCENDING; // 2nd
            // click
          } else if (sortKey.getSortOrder() === phantasus.SortKey.SortOrder.TOP_N) {
            sortOrder = phantasus.SortKey.SortOrder.UNSORTED;
          } else {
            sortOrder = dataType === 'number' || dataType === '[number]' ? phantasus.SortKey.SortOrder.TOP_N : phantasus.SortKey.SortOrder.UNSORTED; // 3rd
            // click
          }

      } else {
        sortKey = new phantasus.SortKey(_this.name,
          phantasus.SortKey.SortOrder.ASCENDING);
        sortOrder = phantasus.SortKey.SortOrder.ASCENDING;
      }
      if (sortKey != null) {
        sortKey.setSortOrder(sortOrder);
        _this.setSortingStatus(_this.getSortKeys(),
          sortKey, additionalSort, isGroupBy);
      }
    });
  $(this.canvas).on('mousedown', function (event) {
    resizeCursor = getResizeCursor(phantasus.CanvasUtil
    .getMousePos(event.target, event, true));
  });
};
phantasus.VectorTrackHeader.FONT_OFFSET = 2;
phantasus.VectorTrackHeader.prototype = {
  selected: false,
  isMouseOver: false,
  defaultFontHeight: 11,
  dispose: function () {
    phantasus.AbstractCanvas.prototype.dispose.call(this);
    this.hammer.off('longpress', this.longpress).off('panend', this.panend).off('panstart',
      this.panstart).off('panmove', this.panmove).off('tap', this.tap);
    this.hammer.destroy();
  },
  getPreferredSize: function () {
    var size = this.getPrintSize();
    size.width += 24; // leave space for sort, drag icon, lock

    if (!this.isColumns) {
      size.height = this.defaultFontHeight
        + phantasus.VectorTrackHeader.FONT_OFFSET;
    }
    // var vector = (this.isColumns ? this.project.getFullDataset()
    // .getColumnMetadata() : this.project.getFullDataset()
    // .getRowMetadata()).getByName(this.name);
    // if (vector
    // && vector.getProperties().get(
    // phantasus.VectorKeys.SHOW_HEADER_SUMMARY)) {
    // if (isNaN(size.height)) {
    // size.height = 0;
    // }
    // if (!this.isColumns) {
    // size.height += 50;
    // } else {
    // size.width += 50;
    // }
    //
    // }
    return size;
  },
  getPrintSize: function () {
    var context = this.canvas.getContext('2d');
    context.font = this.fontWeight + ' ' + this.defaultFontHeight + 'px '
      + phantasus.CanvasUtil.getFontFamily(context);
    var textWidth = 4 + context.measureText(this.name).width;
    return {
      width: textWidth,
      height: this.defaultFontHeight
      + phantasus.VectorTrackHeader.FONT_OFFSET
    };
  },
  getSortKeys: function () {
    return this.isColumns ? this.project.getColumnSortKeys() : this.project
      .getRowSortKeys();
  },
  setOrder: function (sortKeys) {
    if (this.isColumns) {
      this.project.setColumnSortKeys(phantasus.SortKey
        .keepExistingSortKeys(sortKeys, this.project
          .getColumnSortKeys()), false);
    } else {
      this.project.setRowSortKeys(phantasus.SortKey.keepExistingSortKeys(
        sortKeys, this.project.getRowSortKeys()), false);
    }
  },
  setGroupBy: function (groupBy) {
    var existingGroupBy = this.isColumns ? this.project.groupColumns
      : this.project.groupRows;
    // see if already exists, if so remove it
    var index = -1;
    for (var i = 0, length = existingGroupBy.length; i < length; i++) {
      if (existingGroupBy[i].toString() === groupBy.toString()) {
        index = i;
        break;
      }
    }
    var newGroupBy = [groupBy];
    if (index !== -1) {
      newGroupBy = existingGroupBy;
      newGroupBy.splice(index, 1);
    }
    if (this.isColumns) {
      this.project.setGroupColumns(newGroupBy, true);
    } else {
      this.project.setGroupRows(newGroupBy, true);
    }
  },
  setSelected: function (selected) {
    if (selected != this.selected) {
      this.selected = selected;
      $(this.canvas)
        .css(
          {
            'background-color': this.selected ? this.selectedBackgroundColor
              : this.backgroundColor
          });
    }
  },
  setSortingStatus: function (sortKeys, sortKey, additionalSort, isGroupBy) {
    if (!isGroupBy) {
      if (sortKey.getSortOrder() == phantasus.SortKey.SortOrder.UNSORTED
        && !additionalSort) {
        this.setOrder([]);
      } else {
        if (additionalSort && sortKeys.length == 0) {
          additionalSort = false;
        }
        if (!additionalSort) {
          sortKeys = [sortKey];
        } else {
          var sortKeyIndex = this.getSortKeyIndexForColumnName(
            sortKeys, sortKey.toString());
          if (sortKeyIndex === null) { // new sort column
            sortKeys.push(sortKey);
          } else { // change sort order of existing sort column
            sortKeys[sortKeyIndex.index] = sortKey;
          }
        }
        this.setOrder(sortKeys);
      }
    }
    if (isGroupBy) {
      this.setGroupBy(sortKey);
    } else {
      if (this.isColumns) {
        this.project.trigger('columnSortOrderChanged');
      } else {
        this.project.trigger('rowSortOrderChanged');
      }
    }
  },
  getSortKeyIndexForColumnName: function (sortKeys, columnName) {
    if (sortKeys != null) {
      var counter = 0;
      for (var i = 0, size = sortKeys.length; i < size; i++) {
        if (sortKeys[i].isUnlockable()) {
          counter++;
        }
        if (sortKeys[i] instanceof phantasus.SortKey && columnName === sortKeys[i].toString()) {
          return {
            index: i,
            number: counter
          };
        }
      }
    }
    return null;
  },
  print: function (clip, context) {
    if (clip.height <= 6) {
      return;
    }
    context.textBaseline = 'bottom';
    if (this.isColumns) {
      context.textAlign = 'right';
      context.font = this.font.weight + ' ' + Math.min(this.defaultFontHeight, clip.height
        - phantasus.VectorTrackHeader.FONT_OFFSET)
        + 'px ' + phantasus.CanvasUtil.getFontFamily(context);
    } else {
      context.textAlign = 'left';
      context.font = (clip.height - phantasus.VectorTrackHeader.FONT_OFFSET)
        + 'px ' + phantasus.CanvasUtil.getFontFamily(context);
    }
    context.fillStyle = phantasus.CanvasUtil.FONT_COLOR;
    context.fillText(this.name, 0, 0);
  },
  draw: function (clip, context) {
    var sortKeys = this.getSortKeys();
    var name = this.name;
    var existingSortKeyIndex = this.getSortKeyIndexForColumnName(sortKeys,
      name);
    var unlockableSortKeys = sortKeys.filter(function (key) {
      return key.isUnlockable();
    });
    phantasus.CanvasUtil.resetTransform(context);
    context.clearRect(0, 0, this.getUnscaledWidth(), this
      .getUnscaledHeight());

    if (this.getUnscaledHeight() < 5) {
      return;
    }

    context.strokeStyle = '#ddd';
    if (this.isColumns) {
      context.beginPath();
      context.moveTo(0, this.getUnscaledHeight());
      context.lineTo(this.getUnscaledWidth(), this.getUnscaledHeight());
      context.stroke();
      context.textAlign = 'right';
    } else {
      context.beginPath();
      context.moveTo(this.getUnscaledWidth(), 0);
      context.lineTo(this.getUnscaledWidth(), this.getUnscaledHeight());
      context.stroke();
      context.textAlign = 'left';
    }

    var textWidth = context.measureText(name).width;
    var isColumns = this.isColumns;
    var xpix = this.isColumns ? this.getUnscaledWidth() - 2 : 10;
    if (isColumns) {
      if (existingSortKeyIndex != null) {
        xpix -= 6;
        if (sortKeys[existingSortKeyIndex.index].getLockOrder() !== 0) {
          xpix -= 10;
        }
      }
      if (sortKeys.length > 1) {
        xpix -= 6;
      }

    }
    context.fillStyle = phantasus.CanvasUtil.FONT_COLOR;
    var ypix = this.isColumns ? (this.getUnscaledHeight() / 2)
      : (this.getUnscaledHeight() - (this.defaultFontHeight + phantasus.VectorTrackHeader.FONT_OFFSET) / 2);
    context.textBaseline = 'middle';
    if (this.isMouseOver) {
      var xdot = xpix - (isColumns ? textWidth + 4 : 4);
      var ydot = ypix - 3;
      for (var i = 0; i < 2; i++) {
        for (var j = 0; j < 3; j++) {
          context.fillRect(xdot - i * 3, ydot + j * 3, 1.5, 1.5);
        }
      }
    }
    var fontHeight = Math.min(this.defaultFontHeight, this
        .getUnscaledHeight()
      - phantasus.VectorTrackHeader.FONT_OFFSET);
    fontHeight = Math.min(fontHeight, phantasus.VectorTrack.MAX_FONT_SIZE);
    context.font = this.font.weight + ' ' + fontHeight + 'px ' + phantasus.CanvasUtil.getFontFamily(context);
    context.fillText(name, xpix, ypix);
    context.fillStyle = phantasus.CanvasUtil.FONT_COLOR;
    if (existingSortKeyIndex !== null) {
      // draw arrow
      context.beginPath();
      var x = this.isColumns ? xpix + 4 : xpix + textWidth + 6;
      var arrowHeight = Math.min(8, this.getUnscaledHeight() / 2 - 1);
      var arrowWidth = 3;
      if (sortKeys[existingSortKeyIndex.index].getSortOrder() === phantasus.SortKey.SortOrder.ASCENDING) {
        // up arrow
        context.translate(x, ypix - arrowHeight);
        context.moveTo(0, 0);
        context.lineTo(arrowWidth, arrowHeight);
        context.lineTo(-arrowWidth, arrowHeight);
      } else if (sortKeys[existingSortKeyIndex.index].getSortOrder() === phantasus.SortKey.SortOrder.DESCENDING) { // down
        // arrow
        context.translate(x, ypix);
        context.moveTo(0, arrowHeight);
        context.lineTo(arrowWidth, 0);
        context.lineTo(-arrowWidth, 0);
      } else { // diamond
        context.translate(x, ypix - arrowHeight / 2);
        context.moveTo(0, 0);
        context.lineTo(arrowWidth, arrowHeight / 2);
        context.lineTo(0, arrowHeight);
        context.lineTo(-arrowWidth, arrowHeight / 2);
      }
      context.fill();
      phantasus.CanvasUtil.resetTransform(context);
      context.textAlign = 'left';
      if (unlockableSortKeys.length > 1) {
        context.font = '8px ' + phantasus.CanvasUtil.getFontFamily(context);
        var sortIndex = '' + (existingSortKeyIndex.number);
        context.fillText(sortIndex, x + 4,
          ypix - 2);
        x += context.measureText(sortIndex).width;
      }
      if (sortKeys[existingSortKeyIndex.index].getLockOrder() !== 0) {
        context.font = fontHeight + 'px FontAwesome';
        context.fillText('\uf023', x + arrowWidth + 2, ypix);
      }

    }
  }
};
phantasus.Util.extend(phantasus.VectorTrackHeader, phantasus.AbstractCanvas);

phantasus.VectorTrack = function (project, name, positions, isColumns, heatmap) {
  phantasus.AbstractCanvas.call(this, true);
  this.preferredSize = {
    width: 0,
    height: 0
  };
  this.project = project;
  this.positions = positions;
  this.isColumns = isColumns;
  this.name = name;
  this.visible = true;
  this.heatmap = heatmap;

  // this.highlightColor = 'rgb(255,255,0)';
  this.id = _.uniqueId();
  var _this = this;
  this.updateSpanMapFunction = function () {
    _this.spanMap = phantasus.VectorUtil.createSpanMap(_this.getVector());
  };

  this.lastPosition = {
    start: -1,
    end: -1
  };
  // for molecule span
  this.events = 'rowSortOrderChanged rowFilterChanged datasetChanged';
  var mouseMoved = function (event) {
    var index = -1;
    if (event.type !== 'mouseout') {
      var position = phantasus.CanvasUtil.getMousePosWithScroll(
        event.target, event, heatmap.scrollLeft(), heatmap
          .scrollTop());
      if (_this.settings.squished) {
        var total = positions.getPosition(positions.getLength() - 1)
          + positions.getItemSize(positions.getLength() - 1);
        var squishFactor = total
          / (isColumns ? _this.getUnscaledWidth() : _this
            .getUnscaledHeight());
        position[isColumns ? 'x' : 'y'] *= squishFactor;
      }
      index = !isColumns ? _this.positions.getIndex(position.y, false)
        : _this.positions.getIndex(position.x, false);

    }

    if (isColumns) {
      heatmap.setMousePosition(-1, index, {
        name: _this.name,
        event: event
      });
    } else {
      heatmap.setMousePosition(index, -1, {
        name: _this.name,
        event: event

      });
    }
  };

  $(this.canvas).on('contextmenu.phantasus', function (e) {
    e.preventDefault();
    e.stopPropagation();
    e.stopImmediatePropagation();
    heatmap.setSelectedTrack(_this.name, isColumns);
    _this.showPopup(e);
    return false;

  });
  // display options:
  // - text and color, shape, bar (stacked or not), highlight matching (any
  // except arrays)
  // - color and bar-discrete or contin
  // - color by for bar plots
  this.settings = {
    maxTextWidth: undefined,
    squished: false,
    inlineTooltip: false,
    tooltip: true,
    highlightMatchingValues: false,
    colorBarSize: 12,
    stackedBar: false,
    display: [],
    // selectionColor: 'rgb(182,213,253)',
    selectionColor: 'rgb(255,125,0)',
    colorByField: null, // color this vector by another vector, used in bar plot and text
    fontField: null, // use a different field for determining font
    barColor: '#bdbdbd',
    barSize: 40,
    min: undefined,
    mid: undefined,
    max: undefined,
    autoscaleAlways: false, // autoscale on every repaint
    minMaxReversed: false
    // whether to reverse min and max when auto-setting min and max
  };
  $(this.canvas).on('mousemove.phantasus mouseout.phantasus', mouseMoved);
};
phantasus.VectorTrack.RENDER = {
  TEXT: 'text',
  COLOR: 'color',
  BAR: 'bar',
  MOLECULE: 'molecule',
  TEXT_AND_COLOR: 'text_and_color',
  TEXT_AND_FONT: 'text_and_font',
  SHAPE: 'shape',
  ARC: 'arc',
  BOX_PLOT: 'box_plot',
  HEAT_MAP: 'heat_map'
};
phantasus.VectorTrack.vectorToString = function (vector) {
  var dataType = phantasus.VectorUtil.getDataType(vector);
  var formatter = vector.getProperties().get(phantasus.VectorKeys.FORMATTER);
  if (formatter != null) {
    if (typeof formatter === 'object') { // convert to function
      formatter = phantasus.Util.createNumberFormat(formatter.pattern);
      if (dataType === '[number]') {
        var nf = formatter;
        formatter = function (v) {
          var s = [];
          if (v != null) {
            for (var i = 0, arrayLength = v.length; i < arrayLength; i++) {
              s.push(nf(v[i]));
            }
          }
          return s.join(', ');
        };
      }
      vector.getProperties().set(phantasus.VectorKeys.FORMATTER, formatter);
    }
  } else if (dataType === 'number') {
    formatter = phantasus.Util.nf;
  } else if (dataType === '[number]') {
    formatter = function (v) {
      var s = [];
      if (v != null) {
        for (var i = 0, arrayLength = v.length; i < arrayLength; i++) {
          s.push(phantasus.Util.nf(v[i]));
        }
      }
      return s.join(', ');
    };
  } else if (dataType === '[string]') {
    formatter = function (v) {
      var s = [];
      if (v != null) {
        for (var i = 0, arrayLength = v.length; i < arrayLength; i++) {
          s.push(v[i]);
        }
      }
      return s.join(', ');
    };
  } else {
    formatter = function (v) {
      return '' + v;
    };
  }
  return formatter;
};

phantasus.VectorTrack.prototype = {
  settingFromConfig: function (conf) {
    var _this = this;
    var settings = this.settings;
    // new style= rows:[{field: 'test', display:['text']}]
    // old style= rows:[{field: 'test', display:'text,color'}]
    var fromString = function (s) {
      settings.display = [];
      var tokens = s.split(',');
      for (var i = 0, length = tokens.length; i < length; i++) {
        var method = $.trim(tokens[i]);
        method = method.toUpperCase();
        var mapped = phantasus.VectorTrack.RENDER[method];
        if (mapped !== undefined) {
          settings.display.push(mapped);
        } else if (method === 'DISCRETE') {
          _this.getFullVector().getProperties().get(phantasus.VectorKeys.DISCRETE, true);
        } else if (method === 'CONTINUOUS') {
          _this.getFullVector().getProperties().set(phantasus.VectorKeys.DISCRETE, false);
        } else if (method === 'HIGHLIGHT') {
          settings.highlightMatchingValues = true;
        } else if (method === 'STACKED_BAR') {
          settings.stackedBar = true;
          settings.display.push(phantasus.VectorTrack.RENDER.BAR);
        } else if (method === 'TOOLTIP') {
          settings.inlineTooltip = true;
        } else {
          // console.log(method + ' not found.');
        }
      }
    };
    var fromArray = function (array) {
      settings.display = [];
      for (var i = 0; i < array.length; i++) {
        var method = array[i].toUpperCase();
        var mapped = phantasus.VectorTrack.RENDER[method];
        if (mapped !== undefined) {
          settings.display.push(mapped);
        } else {
          console.log(method + ' not found.');
        }
      }
    };
    var fromObject = function (obj) {
      settings.display = [];
      for (var key in obj) {
        if (obj[key]) {
          var method = key.toUpperCase();
          var mapped = phantasus.VectorTrack.RENDER[method];
          if (mapped !== undefined) {
            settings.display.push(mapped);
          }
        }
      }
    };

    if (conf != null) {
      if (_.isString(conf)) { // deprecated, comma separated list of text, color, etc
        fromString(conf);
      } else if (_.isArray(conf)) {
        fromArray(conf);
      } else {
        var userSuppliedSettings = conf;
        if (!_.isArray(conf.display) && _.isObject(conf.display)) { // deprecated
          userSuppliedSettings = conf.display;
        }
        settings = $.extend({}, settings, userSuppliedSettings);
        settings.maxTextWidth = undefined;
        if (userSuppliedSettings.discrete != null) {
          _this.getFullVector().getProperties().get(phantasus.VectorKeys.DISCRETE, userSuppliedSettings.discrete);
        }

        if (_.isArray(userSuppliedSettings.display)) {
          settings.display = userSuppliedSettings.display;
        } else if (_.isString(userSuppliedSettings.display)) {
          fromString(userSuppliedSettings.display);
        }
        if (!_.isArray(settings.render) && _.isObject(settings.render)) {// deprecated
          fromObject(settings.render);
          delete settings.render;
        }
      }
      this.settings = settings;
    }
    if (!this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT) &&
      (this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_COLOR) || this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_FONT))) {
      settings.display.push(phantasus.VectorTrack.RENDER.TEXT);
    }
    this._update();

  },
  setShowTooltip: function (value) {
    this.settings.tooltip = value;
  },
  isShowTooltip: function () {
    return this.settings.tooltip;
  },
  isRenderAs: function (value) {
    return this.settings.display.indexOf(value) !== -1;
  },
  dispose: function () {
    phantasus.AbstractCanvas.prototype.dispose.call(this);
    $(this.canvas).off();
    this._selection.dispose();
    this.project.off(this.events, this.updateSpanMapFunction);
  },
  getName: function () {
    return this.name;
  },
  getVector: function (name) {
    name = name == null ? this.name : name;
    var vector = this.isColumns ? this.project.getSortedFilteredDataset()
      .getColumnMetadata().getByName(name) : this.project
      .getSortedFilteredDataset().getRowMetadata().getByName(name);
    return !vector ? new phantasus.Vector(name, 0) : vector;
  },
  getFullVector: function () {
    var vector = this.isColumns ? this.project.getFullDataset()
      .getColumnMetadata().getByName(this.name) : this.project
      .getFullDataset().getRowMetadata().getByName(this.name);
    return !vector ? new phantasus.Vector(this.name, 0) : vector;
  },
  _updatePreferredSize: function () {
    var size = this._computePreferredSize();
    this.preferredSize.width = size.width;
    this.preferredSize.height = size.height;

  },
  _computePreferredSize: function (forPrint) {
    var width = 0;
    var height = 0;
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT)) {
      if (this.positions.getSize() >= 6) {
        var context = this.canvas.getContext('2d');
        var textWidth = phantasus.CanvasUtil.getVectorStringWidth(
          context, this.getVector(), this.positions,
          forPrint ? -1 : (this.isColumns ? 120 : 100));
        if (!forPrint) {
          this.settings.maxTextWidth = textWidth;
        }
        width += textWidth;
      } else if (!forPrint) {
        this.settings.maxTextWidth = 0; // text not drawn
      }
    }
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.BAR)) {
      width += this.settings.barSize;
    }
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.COLOR)) {
      width += this.settings.colorBarSize;
    }
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.SHAPE)) {
      width += this.settings.colorBarSize;
    }
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.MOLECULE)) {
      width += 300;
    }
    // if (!forPrint && !this.isRenderAs(phantasus.VectorTrack.RENDER.MOLECULE)) {
    //   width = Math.min(300, width);
    // }
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.ARC)) {
      width += this.settings.arcSize;
    }
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.BOX_PLOT)) {
      width += 100;
    }
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.HEAT_MAP)) {
      width += 100;
    }
    // 2 pixel spacing between display types
    var nkeys = this.settings.display.length;

    if (nkeys > 0) {
      width += (nkeys - 1) * 2;
    }
    width = Math.max(0, width);
    return this.isColumns ? {
      width: height,
      height: width
    } : {
      width: width,
      height: height
    };

  },
  getPreferredSize: function () {
    return this.preferredSize;
  },
  getPrintSize: function () {
    return this._computePreferredSize(true);
  },
  _createDiscreteValueMap: function () {
    var values = phantasus.VectorUtil.getValues(this.getFullVector());
    values.sort(phantasus.SortKey.ASCENDING_COMPARATOR);
    this.discreteValueMap = new phantasus.Map();
    for (var i = 0, length = values.length; i < length; i++) {
      this.discreteValueMap.set(values[i], i + 1);
    }
    this.settings.min = 0;
    this.settings.mid = 0;
    this.settings.max = values.length;
  },
  _setChartMinMax: function () {
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.BAR)
      || this.isRenderAs(phantasus.VectorTrack.RENDER.BOX_PLOT)) {
      if (!this.settings.stackedBar && this.getFullVector().getProperties().get(phantasus.VectorKeys.DISCRETE)
        && !this.isRenderAs(phantasus.VectorTrack.RENDER.BOX_PLOT)) {
        if (!this.discreteValueMap) {
          this._createDiscreteValueMap();
        }
      } else {
        if (this.settings.autoscaleAlways || this.settings.min == null || this.settings.max == null
          || this.settings.mid == null) {
          var vector = this.getFullVector();

          var minMax = phantasus.VectorUtil.getMinMax(vector);
          var min = minMax.min;
          var max = minMax.max;
          if (this.settings.minMaxReversed) {
            var tmp = max;
            max = min;
            min = tmp;
          }
          if (this.settings.autoscaleAlways || this.settings.min == null) {
            this.settings.min = Math.min(0, min);
          }
          if (this.settings.autoscaleAlways || this.settings.max == null) {
            this.settings.max = Math.max(0, max);
          }
          if (this.settings.autoscaleAlways || this.settings.mid == null) {
            this.settings.mid = this.settings.min < 0 ? 0
              : this.settings.min;
          }
        }

      }
    }
  },

  _update: function () {
    if (this.getFullVector().getProperties().get(phantasus.VectorKeys.DISCRETE) == null
      && (this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_COLOR)
        || this.isRenderAs(phantasus.VectorTrack.RENDER.COLOR) || this
          .isRenderAs(phantasus.VectorTrack.RENDER.BAR))) {
      if ((this.isColumns ? this.project.getColumnColorModel() : this.project.getRowColorModel()).getContinuousColorScheme(this.getFullVector()) != null) {
        this.getFullVector().getProperties().set(phantasus.VectorKeys.DISCRETE, false);
        this.settings.highlightMatchingValues = false;
      } else if ((this.isColumns ? this.project.getColumnColorModel() : this.project.getRowColorModel()).getDiscreteColorScheme(this.getFullVector()) != null) {
        this.getFullVector().getProperties().set(phantasus.VectorKeys.DISCRETE, true);
        this.settings.highlightMatchingValues = true;
      } else if (this.getFullVector().getProperties().has(
          phantasus.VectorKeys.FIELDS)
        || phantasus.VectorUtil.getDataType(this.getFullVector()) === 'number' || phantasus.VectorUtil.getDataType(this.getFullVector()) === '[number]') {
        this.getFullVector().getProperties().set(phantasus.VectorKeys.DISCRETE, false);
        this.settings.highlightMatchingValues = false;
      }
    }
    if (this.getFullVector().getProperties().get(phantasus.VectorKeys.DISCRETE) == null) {
      this.getFullVector().getProperties().set(phantasus.VectorKeys.DISCRETE, true);
    }
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.MOLECULE)) {
      this.project.off(this.events, this.updateSpanMapFunction);
    }
    this._setChartMinMax();
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.MOLECULE)) {
      this.project.on(this.events, this.updateSpanMapFunction);
      if (!this.moleculeCache) {
        this.moleculeCache = {};
        var _this = this;

        var valueToModelIndices = this.getFullVector().getProperties()
          .get(phantasus.VectorKeys.VALUE_TO_INDICES);
        if (!valueToModelIndices) {
          var fullVector = this.getFullVector();
          valueToModelIndices = phantasus.VectorUtil
            .createValueToIndicesMap(fullVector);
          fullVector.getProperties().set(
            phantasus.VectorKeys.VALUE_TO_INDICES,
            valueToModelIndices);

        }

        if (_this.heatmap.options.structureUrlProvider !== undefined) {
          valueToModelIndices.forEach(function (indices, value) {
            var url = _this.heatmap.options
              .structureUrlProvider(value);
            var image = new Image();
            image.src = url;
            _this.moleculeCache[value] = image;
          });

          setTimeout(function () {
            _this.setInvalid(true);
            _this.repaint();
          }, 2000);
        } else {
          var values = valueToModelIndices.keys();
          var doRequest = function (smile) {
            $
              .ajax(
                {
                  contentType: 'text/plain',
                  context: {
                    smile: smile
                  },
                  data: {
                    'string': smile,
                    'representation': 'sdf'
                  },
                  url: 'http://cactus.nci.nih.gov/chemical/structure'
                }).done(function (text) {
              _this.moleculeCache[this.smile] = text;
              if (values.length > 0) {
                doRequest(values.pop());
              }
              _this.invalid = true;
              _this.repaint();
            });
          };
          for (var i = 0; i < 6; i++) {
            doRequest(values.pop());
          }
        }
        this.updateSpanMapFunction();
      }
    }
    this._updatePreferredSize();
  }
  ,
  postPaint: function (clip, context) {
    // draw hover, matching values
    context.lineWidth = 1;
    context.strokeStyle = 'Grey';
    var project = this.project;
    var setup = this._setup(context, clip);
    var vector = setup.vector;
    var start = setup.start;
    var end = setup.end;

    // hover
    if (this.isColumns) {
      if (project.getHoverColumnIndex() !== -1) {
        this.drawColumnBorder(context, this.positions, project
          .getHoverColumnIndex(), this.getUnscaledHeight());

      }
    } else {
      if (project.getHoverRowIndex() !== -1) {
        this.drawRowBorder(context, this.positions, project
          .getHoverRowIndex(), this.getUnscaledWidth());
      }
    }
    this._highlightMatchingValues(context, vector, start, end);
  }
  ,
  _highlightMatchingValues: function (context, viewVector, start, end) {
    var project = this.project;
    var positions = this.positions;
    context.strokeStyle = 'black';
    context.lineWidth = 3;
    var hoverIndex = this.isColumns ? project.getHoverColumnIndex()
      : project.getHoverRowIndex();
    var value = viewVector.getValue(hoverIndex);

    if (this.settings.highlightMatchingValues
      && hoverIndex !== -1
      && this.heatmap.mousePositionOptions
      && this.heatmap.mousePositionOptions.name === viewVector
        .getName()) {
      var valueToModelIndices = this.getFullVector().getProperties().get(
        phantasus.VectorKeys.VALUE_TO_INDICES);
      if (!valueToModelIndices) {
        var fullVector = this.getFullVector();
        valueToModelIndices = phantasus.VectorUtil
          .createValueToIndicesMap(fullVector);
        fullVector.getProperties().set(
          phantasus.VectorKeys.VALUE_TO_INDICES,
          valueToModelIndices);

      }
      var indices = valueToModelIndices.get(value);
      if (indices == null) {
        // console.log('valueToModelIndices error');
        return;
      }
      if (indices.length <= 1) {
        return;
      }
      if (this.isColumns) {
        if (project.getHoverColumnIndex() !== -1) {
          var height = this.getUnscaledHeight();
          // context.fillStyle = '#ffffb3';
          context.beginPath();
          for (var i = 0, nindices = indices.length; i < nindices; i++) {
            var viewIndex = project
              .convertModelColumnIndexToView(indices[i]);
            if (viewIndex >= start && viewIndex < end) {
              var size = positions.getItemSize(viewIndex);
              var pix = positions.getPosition(viewIndex);
              context.rect(pix, 0, size, height);
            }
          }
          context.stroke();

        }
      } else {
        context.beginPath();
        var width = this.getUnscaledWidth();
        var indices = valueToModelIndices.get(value);
        for (var i = 0, nindices = indices.length; i < nindices; i++) {
          var viewIndex = project
            .convertModelRowIndexToView(indices[i]);
          if (viewIndex >= start && viewIndex < end) {
            var size = positions.getItemSize(viewIndex);
            var pix = positions.getPosition(viewIndex);
            context.rect(0, pix, width, size);
          }
        }
        context.stroke();
      }

    }

  }
  ,
  drawSelection: function (options) {
    var project = this.project;
    var positions = this.positions;
    var context = options.context;
    var start = options.start;
    var end = options.end;
    context.lineWidth = 1;
    context.fillStyle = this.settings.selectionColor;
    if (this.isColumns) {
      var height = this.getUnscaledHeight();
      var viewIndices = project.getColumnSelectionModel()
        .getViewIndices();
      viewIndices.forEach(function (i) {
        if (i >= start && i <= end) {
          var size = positions.getItemSize(i);
          var pix = positions.getPosition(i);
          context.fillRect(pix, 0, size, height);
        }
      });
    } else {
      var width = this.getUnscaledWidth();
      if (!this.isRenderAs(phantasus.VectorTrack.RENDER.MOLECULE)) {
        var viewIndices = project.getRowSelectionModel()
          .getViewIndices();
        viewIndices.forEach(function (i) {
          if (i >= start && i <= end) {
            var size = Math.ceil(positions.getItemSize(i));
            var pix = positions.getPosition(i);
            context.fillRect(0, pix, width, size);
          }
        });
      }
    }

  }
  ,
  prePaint: function (clip, context) {
    // draw selection
    var project = this.project;
    var positions = this.positions;
    var setup = this._setup(context, clip);
    var start = setup.start;
    var end = setup.end;
    this.drawSelection({
      context: context,
      start: start,
      end: end
    });
    if (this.invalid || start !== this.lastPosition.start
      || end !== this.lastPosition.end) {
      this.lastPosition.start = start;
      this.lastPosition.end = end;
      this.invalid = true;
    }
  }
  ,
  drawRowBorder: function (context, positions, index, gridSize) {
    var size = positions.getItemSize(index);
    var pix = positions.getPosition(index);
    // top and bottom lines
    context.beginPath();
    context.moveTo(0, pix + size);
    context.lineTo(gridSize, pix + size);
    context.stroke();
    context.beginPath();
    context.moveTo(0, pix);
    context.lineTo(gridSize, pix);
    context.stroke();
  }
  ,
  drawColumnBorder: function (context, positions, index, gridSize) {
    var size = positions.getItemSize(index);
    var pix = positions.getPosition(index);
    // top and bottom lines
    context.beginPath();
    context.moveTo(pix + size, 0);
    context.lineTo(pix + size, gridSize);
    context.stroke();
    context.beginPath();
    context.moveTo(pix, 0);
    context.lineTo(pix, gridSize);
    context.stroke();
  }
  ,
  isSquished: function () {
    return this.settings.squished;
  }
  ,
  _setup: function (context, clip) {
    var start = 0;
    var vector = this.getVector();
    var end = vector.size();
    var settings = this.settings;
    var positions = this.positions;
    var width = clip.width;
    var height = clip.height;
    if (!settings.squished) {
      if (this.isColumns) {
        start = phantasus.Positions.getLeft(clip, positions);
        end = phantasus.Positions.getRight(clip, positions);
      } else {
        start = phantasus.Positions.getTop(clip, positions);
        end = phantasus.Positions.getBottom(clip, positions);
      }
    }
    if (settings.squished) {

      var total = positions.getPosition(positions.getLength() - 1)
        + positions.getItemSize(positions.getLength() - 1);
      if (!this.isColumns) {
        var squishFactor = height / total;
        context.scale(1, squishFactor);
      } else {
        var squishFactor = width / total;
        context.scale(squishFactor, 1);
      }

    } else {
      context.translate(-clip.x, -clip.y);
    }
    return {
      start: start,
      end: end,
      vector: vector
    };
  }
  ,
  draw: function (clip, context) {
    var setup = this._setup(context, clip);
    this._draw({
      start: setup.start,
      end: setup.end,
      vector: setup.vector,
      context: context,
      availableSpace: this.isColumns ? this.getUnscaledHeight()
        : this.getUnscaledWidth(),
      clip: clip
    });
  }
  ,
  print: function (clip, context) {
    var vector = this.getVector();
    this._draw({
      start: 0,
      end: vector.size(),
      vector: vector,
      context: context,
      availableSpace: this.isColumns ? clip.height
        : clip.width,
      clip: clip
    });
  }
  ,
  /**
   * @param options.vector
   * @param options.context
   * @param options.start
   * @param options.end
   * @param options.availableSpace
   */
  _draw: function (options) {
    var _this = this;
    var context = options.context;
    var vector = options.vector;
    var availableSpace = options.availableSpace;
    var fullAvailableSpace = options.availableSpace;
    var start = options.start;
    var end = options.end;
    var clip = options.clip;
    var positions = this.positions;
    if (this.settings.autoscaleAlways) {
      this._setChartMinMax();
    }
    context.textAlign = 'left';
    context.fillStyle = phantasus.CanvasUtil.FONT_COLOR;

    var fontSize = Math.min(phantasus.VectorTrack.MAX_FONT_SIZE, positions.getSize() - 2);
    var size = 0;
    context.font = fontSize + 'px ' + phantasus.CanvasUtil.getFontFamily(context);
    context.strokeStyle = phantasus.HeatMapElementCanvas.GRID_COLOR;
    context.lineWidth = 0.1;
    // grid lines
    if (this.heatmap.heatmap.isDrawGrid() && !this.settings.squished) {
      if (this.isColumns) {
        var gridSize = availableSpace;
        context.beginPath();
        for (var i = start; i < end; i++) {
          var size = positions.getItemSize(i);
          var pix = positions.getPosition(i);
          if (size > 7) {
            context.moveTo(pix + size, 0);
            context.lineTo(pix + size, gridSize);
          }
        }
        context.stroke();
      } else {
        if (!this.isRenderAs(phantasus.VectorTrack.RENDER.MOLECULE)) {
          var gridSize = availableSpace;
          context.beginPath();
          for (var i = start; i < end; i++) {
            var size = positions.getItemSize(i);
            var pix = positions.getPosition(i);
            if (size > 7) {

              context.moveTo(0, pix + size);
              context.lineTo(gridSize, pix + size);

            }
          }
          context.stroke();
        }
      }
    }
    context.lineWidth = 1;
    var offset = 1;

    if (this.isRenderAs(phantasus.VectorTrack.RENDER.COLOR)) {
      this.renderColor(context, vector, start, end, clip,
        this.isColumns ? availableSpace : 0,
        !this.getFullVector().getProperties().get(phantasus.VectorKeys.DISCRETE));
      offset += this.settings.colorBarSize + 2;
      availableSpace -= offset;
    }
    if (!this.settings.squished
      && this.isRenderAs(phantasus.VectorTrack.RENDER.SHAPE)) {
      this.renderShape(context, vector, start, end, clip,
        this.isColumns ? availableSpace - offset : offset);
      offset += this.settings.colorBarSize + 2;
      availableSpace -= offset;
    }

    if (this.isRenderAs(phantasus.VectorTrack.RENDER.ARC)) {
      this.renderArc(context, vector, start, end, clip,
        this.settings.arcSize);
      offset += this.settings.arcSize + 2;
      availableSpace -= offset;
    }
    if (!this.settings.squished
      && this.isRenderAs(phantasus.VectorTrack.RENDER.MOLECULE)) {
      this.renderMolecule(context, vector, start, end, clip, offset,
        availableSpace);
    }
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.BOX_PLOT)) {
      var barSize = !this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT) ? (availableSpace - 2)
        : this.settings.barSize;
      offset++;
      this.renderBoxPlot(context, vector, start, end, clip, offset,
        barSize);
      offset += barSize + 2;
      availableSpace -= offset;
    }
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.HEAT_MAP)) {
      var barSize = !this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT) ? (availableSpace - 2)
        : this.settings.barSize;
      offset++;
      this.renderHeatMap(context, vector, start, end, clip,
        barSize);
      offset += barSize + 2;
      availableSpace -= offset;
    }

    if (this.isRenderAs(phantasus.VectorTrack.RENDER.BAR)) {
      var barSize = !this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT) ? (availableSpace - 1)
        : this.settings.barSize;
      if (this.settings.stackedBar) {
        this.renderStackedBar(context, vector, start, end, clip,
          offset, barSize);
      } else {
        var fields = vector.getProperties().get(
          phantasus.VectorKeys.FIELDS);
        var visibleFieldIndices = vector.getProperties().get(
          phantasus.VectorKeys.VISIBLE_FIELDS);
        if (fields != null && visibleFieldIndices == null) {
          visibleFieldIndices = phantasus.Util.seq(fields.length);
        }

        if (fields != null) {
          this.renderUnstackedBar(context, vector, start, end, clip,
            offset, barSize, visibleFieldIndices);
        } else {
          this.renderBar(context, vector, start, end, clip, this.isColumns ? (fullAvailableSpace - offset - barSize) : offset,
            barSize);
        }
      }

      offset += barSize + 2;
      availableSpace -= offset;

    }
    if (!this.settings.squished
      && this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT) &&
      (this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_COLOR) || this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_FONT))) {
      context.fillStyle = phantasus.CanvasUtil.FONT_COLOR;
      this.renderText(context, vector, this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_COLOR), this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_FONT),
        start, end, clip, this.isColumns ? (fullAvailableSpace - offset) : offset);
      offset += this.settings.maxTextWidth + 2;
      availableSpace -= offset;
    }
    if (!this.settings.squished
      && this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT) && !this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_COLOR)
      && !this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_FONT)) {
      context.fillStyle = phantasus.CanvasUtil.FONT_COLOR;
      var dataType = phantasus.VectorUtil.getDataType(vector);
      if (dataType === 'url') {
        context.fillStyle = 'blue';
        this.canvas.style.cursor = 'pointer';
      }
      this.renderText(context, vector, false, false, start, end, clip, this.isColumns ? (fullAvailableSpace - offset) : offset);

    }
  }
  ,
  showPopup: function (e, isHeader) {
    var _this = this;
    var project = this.project;
    var isColumns = this.isColumns;
    var hasSelection = isColumns ? project.getColumnSelectionModel()
      .count() > 0 : project.getRowSelectionModel().count() > 0;
    var ANNOTATE_SELECTION = 'Annotate Selection';
    var INVERT_SELECTION = 'Invert Selection';
    var SELECT_ALL = 'Select All';
    var SHOW_SELECTION_ONLY = 'Show Selection Only';
    var CLEAR_SELECTION = 'Clear Selection';
    var HIGHLIGHT_MATCHING_VALUES = 'Highlight Matching Values';
    var FIELDS = 'Choose Fields...';
    var DELETE = 'Delete...';
    var TOOLTIP = 'Show In Tooltip';
    var HIDE = 'Hide';
    var HIDE_OTHERS = 'Hide Others';
    var REMOVE_SHOW_SELECTION_ONLY = 'Show All';
    var SORT_ASC = 'Sort Ascending';
    var SORT_DESC = 'Sort Descending';
    var MOVE_TO_TOP = 'Move To Top';
    var SORT_SEL_ASC = 'Sort Heat Map Ascending \u2191';
    var SORT_SEL_DESC = 'Sort Heat Map Descending \u2193';
    var SORT_SEL_TOP_N = 'Sort Heat Map Descending/Ascending';
    var DISPLAY_BAR = 'Show Bar Chart';
    var NUMBER_FORMAT = 'Format';
    var DISPLAY_STACKED_BAR = 'Show Stacked Bar Chart';
    var DISPLAY_BOX_PLOT = 'Show Box Plot';
    var DISPLAY_COLOR = 'Show Color';
    var COLOR_BAR_SIZE = 'Color Bar Size...';
    var DISPLAY_TEXT = 'Show Text';
    var DISPLAY_SHAPE = 'Show Shape';
    var DISPLAY_ARC = 'Show Arc';
    var DISPLAY_TEXT_AND_COLOR = 'Encode Text Using Color';
    var DISPLAY_TEXT_AND_FONT = 'Encode Text Using Font';
    var DISPLAY_STRUCTURE = 'Show Chemical Structure';
    var DISPLAY_CONTINUOUS = 'Continuous';
    var positions = this.positions;
    var heatmap = this.heatmap;

    var sectionToItems = {
      'Sort': [],
      'Selection': [],
      'Display': []
    };

    var customItems = this.heatmap.getPopupItems();
    if (customItems && customItems.length > 0) {
      customItems.forEach(function (item) {
        if (item.columns === isColumns) {
          sectionToItems[item.section].push(item);
        }
      });
    }
    if (sectionToItems.Selection.length > 0) {
      sectionToItems.Selection.push({
        separator: true
      });
    }
    sectionToItems.Selection.push({
      name: MOVE_TO_TOP
    });
    if (this.heatmap.options.menu.Edit && this.heatmap.options.menu.Edit.indexOf('Annotate' +
        ' Selected Rows') !== -1) {
      sectionToItems.Selection.push({
        name: ANNOTATE_SELECTION
      });
    }
    sectionToItems.Selection.push({
      name: 'Copy',
      class: 'copy'
    });

    sectionToItems.Selection.push({
      name: INVERT_SELECTION
    });
    sectionToItems.Selection.push({
      name: SELECT_ALL
    });
    sectionToItems.Selection.push({
      name: CLEAR_SELECTION
    });
    // sectionToItems.Selection.push({
    // name : SHOW_SELECTION_ONLY
    // });
    var combinedFilter = isColumns ? project.getColumnFilter() : project
      .getRowFilter();
    var showSelectionOnlyIndex = combinedFilter
      .indexOf(SHOW_SELECTION_ONLY);
    if (showSelectionOnlyIndex !== -1) {
      sectionToItems.Selection.push({
        name: REMOVE_SHOW_SELECTION_ONLY
      });
    }

    if (!isHeader) {
      sectionToItems['Sort'].push({
        name: SORT_SEL_ASC,
        disabled: !hasSelection
      });
      sectionToItems['Sort'].push({
        name: SORT_SEL_DESC,
        disabled: !hasSelection
      });

      sectionToItems['Sort'].push({
        name: SORT_SEL_TOP_N,
        disabled: !hasSelection
      });
    }
    var dataType = phantasus.VectorUtil.getDataType(this.getFullVector());
    var arrayFields = this.getFullVector().getProperties().get(
      phantasus.VectorKeys.FIELDS);
    var isArray = arrayFields !== undefined;
    var isNumber = dataType === 'number' || dataType === '[number]';
    if (isNumber || isArray) {
      sectionToItems.Display.push({
        name: NUMBER_FORMAT
      });
      sectionToItems.Display.push({
        separator: true
      });

      sectionToItems.Display.push({
        name: DISPLAY_BAR,
        checked: this.isRenderAs(phantasus.VectorTrack.RENDER.BAR)
      });
    }
    if (isArray) {
      sectionToItems.Display.push({
        name: DISPLAY_STACKED_BAR,
        checked: this.settings.stackedBar
      });
      sectionToItems.Display.push({
        name: DISPLAY_BOX_PLOT,
        checked: this.isRenderAs(phantasus.VectorTrack.RENDER.BOX_PLOT)
      });
      sectionToItems.Display.push({
        name: FIELDS
      });
    }

    if (dataType !== 'url') {
      // text and text_and_color are mutually exclusive
      sectionToItems.Display.push({
        name: DISPLAY_TEXT,
        checked: this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT)
      });
      if (this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT)) {
        sectionToItems.Display.push({
          name: DISPLAY_TEXT_AND_COLOR,
          checked: this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_COLOR)
        });
        sectionToItems.Display.push({
          name: DISPLAY_TEXT_AND_FONT,
          checked: this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_FONT)
        });
      }
      sectionToItems.Display.push({
        separator: true
      });

      sectionToItems.Display.push({
        name: DISPLAY_COLOR,
        checked: this.isRenderAs(phantasus.VectorTrack.RENDER.COLOR)
      });
      if (this.isRenderAs(phantasus.VectorTrack.RENDER.COLOR)) {
        sectionToItems.Display.push({
          name: COLOR_BAR_SIZE
        });
      }
    }
    if (!isArray && dataType !== 'url') {
      sectionToItems.Display.push({
        name: DISPLAY_SHAPE,
        checked: this.isRenderAs(phantasus.VectorTrack.RENDER.SHAPE)
      });
      // sectionToItems.Display.push({
      // name : DISPLAY_ARC,
      // checked : this.isRenderAs(phantasus.VectorTrack.RENDER.ARC)
      // });

    }

    if (!isArray && !isNumber && !this.isColumns
      && name.toLowerCase().indexOf('smile') !== -1) {
      sectionToItems.Display.push({
        name: DISPLAY_STRUCTURE,
        checked: this.isRenderAs(phantasus.VectorTrack.RENDER.MOLECULE)
      });
    }

    sectionToItems.Display.push({
      name: TOOLTIP,
      checked: this.settings.inlineTooltip
    });
    if (!isArray && dataType !== 'url') {
      sectionToItems.Display.push({
        name: HIGHLIGHT_MATCHING_VALUES,
        checked: this.settings.highlightMatchingValues
      });
    }
    if (dataType !== 'url') {
      // sectionToItems.Display.push({
      //   name: 'Squished',
      //   checked: this.settings.squished
      // });
    }
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.BAR)
      || this.isRenderAs(phantasus.VectorTrack.RENDER.BOX_PLOT)) {
      sectionToItems.Display.push({
        separator: true
      });
      if (this.isRenderAs(phantasus.VectorTrack.RENDER.BAR)) {
        sectionToItems.Display.push({
          name: 'Edit Bar Color...'
        });
      }
      sectionToItems.Display.push({
        name: 'Auto Range'
      });
      sectionToItems.Display.push({
        name: 'Custom Range...'
      });
    }
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.COLOR)
      || this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_COLOR) || this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_FONT)
      || this.isRenderAs(phantasus.VectorTrack.RENDER.BAR)
      || this.isRenderAs(phantasus.VectorTrack.RENDER.SHAPE)) {
      sectionToItems.Display.push({
        separator: true
      });
      if (isNumber) {
        sectionToItems.Display.push({
          name: DISPLAY_CONTINUOUS,
          checked: !this.getFullVector().getProperties().get(phantasus.VectorKeys.DISCRETE)
        });
      }
      if (this.isRenderAs(phantasus.VectorTrack.RENDER.COLOR)
        || this
          .isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_COLOR)
        || (this.isRenderAs(phantasus.VectorTrack.RENDER.BAR) && isArray)) {
        sectionToItems.Display.push({
          name: 'Edit Colors...'
        });

      }
      if (this.isRenderAs(phantasus.VectorTrack.RENDER.SHAPE)) {
        sectionToItems.Display.push({
          name: 'Edit Shapes...'
        });

      }
      if (this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_FONT)) {
        sectionToItems.Display.push({
          name: 'Edit Fonts...'
        });

      }

      if (this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_FONT)) {
        sectionToItems.Display.push({
          name: 'Edit Fonts...'
        });

      }

      if (this.isRenderAs(phantasus.VectorTrack.RENDER.COLOR)
        || this
          .isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_COLOR)
        || (this.isRenderAs(phantasus.VectorTrack.RENDER.BAR) && isArray)) {
        sectionToItems.Display.push({
          name: 'Color Key',
          icon: 'fa fa-key'
        });
      }

    }
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.SHAPE)) {
      sectionToItems.Display.push({
        name: 'Shape Key',
        icon: 'fa fa-key'
      });
    }
    if (this.isRenderAs(phantasus.VectorTrack.RENDER.TEXT_AND_FONT)) {
      sectionToItems.Display.push({
        name: 'Font Key',
        icon: 'fa fa-key'
      });
    }

    sectionToItems.Display.push({
      separator: true
    });
    sectionToItems.Display.push({
      name: HIDE
    });
    sectionToItems.Display.push({
      name: HIDE_OTHERS,
      disabled: heatmap.getVisibleTrackNames(this.isColumns).length <= 1

    });
    sectionToItems.Display.push({
      separator: true
    });
    sectionToItems.Display.push({
      name: DELETE
    });

    var items = [];

    function addSection(name) {
      if (items.length > 0) {
        items.push({
          separator: true
        });
      }
      items = items.concat(sectionToItems[name]);
    }

    addSection('Sort');
    _.each(sectionToItems.Selection, function (item) {
      if (item.name !== REMOVE_SHOW_SELECTION_ONLY
        && item.name !== SELECT_ALL) {
        item.disabled = !hasSelection;
      }
    });
    if (!isHeader) {
      addSection('Selection');
    } else if (this.heatmap.options.toolbar.options) {
      addSection('Display');
    }

    if (e.preventDefault) {
      e.preventDefault();
    }
    if (items.length === 0) {
      return;
    }
    phantasus.Popup
    .showPopup(
      items,
      {
        x: e.pageX,
        y: e.pageY
      },
      e.target,
      function (event, item) {
        var customItem;
        if (item === NUMBER_FORMAT) {
          var vector = _this.getFullVector();
          var formatter = vector
          .getProperties().get(phantasus.VectorKeys.FORMATTER);
          if (formatter != null) {
            if (typeof formatter === 'object') { // convert to function
              formatter = phantasus.Util.createNumberFormat(formatter.pattern);
              vector.getProperties().set(phantasus.VectorKeys.FORMATTER, formatter);
            }
          }
          var pattern = formatter != null ? formatter.toJSON().pattern : '.5g';
          var formBuilder = new phantasus.FormBuilder();
          formBuilder.append({
            name: 'number_of_fraction_digits',
            type: 'number',
            value: phantasus.Util.getNumberFormatPatternFractionDigits(pattern),
            required: true,
            style: 'max-width:60px;'
          });
          formBuilder.find('number_of_fraction_digits').on(
            'keyup input', _.debounce(
              function () {
                var n = parseInt($(this)
                .val());
                if (n >= 0) {
                  vector.getProperties().set(phantasus.VectorKeys.FORMATTER, {pattern: '.' + n + 'g'});
                  _this.setInvalid(true);
                  _this.repaint();
                }
              }, 100));
          phantasus.FormBuilder.showInModal({
            title: 'Format',
            close: 'Close',
            html: formBuilder.$form,
            focus: heatmap.getFocusEl()
          });
        } else if (item === 'Copy') {
          heatmap.getActionManager().execute(isColumns ? 'Copy Selected Columns' : 'Copy' +
            ' Selected Rows');
        } else if (item === FIELDS) {
          var visibleFieldIndices = _this
          .getFullVector()
          .getProperties()
          .get(phantasus.VectorKeys.VISIBLE_FIELDS);
          var visibleFields;
          if (visibleFieldIndices == null) {
            visibleFields = arrayFields.slice(0);
          } else {
            visibleFields = [];
            for (var i = 0; i < visibleFieldIndices.length; i++) {
              visibleFields
              .push(arrayFields[visibleFieldIndices[i]]);
            }

            }
            var availableFields = [];
            for (var i = 0; i < arrayFields.length; i++) {
              if (visibleFields.indexOf(arrayFields[i]) === -1) {
                availableFields.push(arrayFields[i]);
              }
            }

            var leftOptions = [];
            var rightOptions = [];
            for (var i = 0; i < availableFields.length; i++) {
              leftOptions.push(new Option(
                availableFields[i],
                availableFields[i]));
            }
            for (var i = 0; i < visibleFields.length; i++) {
              rightOptions
                .push(new Option(visibleFields[i],
                  visibleFields[i]));
            }

            var list = new phantasus.DualList(leftOptions,
              rightOptions);

          phantasus.FormBuilder
          .showOkCancel({
            title: 'Fields',
            okCallback: function () {
              var visibleFields = list
              .getOptions(false);
              var visibleFieldIndices = [];
              for (var i = 0; i < visibleFields.length; i++) {
                visibleFieldIndices
                .push(arrayFields
                .indexOf(visibleFields[i]));
              }
              var fullVector = _this
              .getFullVector();
              fullVector
              .getProperties()
              .set(
                phantasus.VectorKeys.VISIBLE_FIELDS,
                visibleFieldIndices);

              var summaryFunction = fullVector
              .getProperties()
              .get(
                phantasus.VectorKeys.ARRAY_SUMMARY_FUNCTION);
              if (summaryFunction) {
                summaryFunction.indices = visibleFieldIndices;
              }
              var updatedVector = _this.isColumns ? _this.project
                .getFullDataset()
                .getColumnMetadata()
                .add(_this.name)
                : _this.project
                .getFullDataset()
                .getRowMetadata()
                .add(_this.name);
              // remove cached summary field
              for (var i = 0; i < updatedVector
              .size(); i++) {
                var array = fullVector
                .getValue(i);
                if (array != null) {
                  array.summary = undefined;
                }

                  }

              _this.setInvalid(true);
              _this.repaint();
            },
            content: list.$el
          });
        } else if (item === 'Edit Bar Color...') {
          var formBuilder = new phantasus.FormBuilder();
          formBuilder.append({
            name: 'bar_color',
            type: 'color',
            value: _this.settings.barColor,
            required: true,
            style: 'max-width:50px;'
          });
          formBuilder.find('bar_color').on(
            'change',
            function () {
              _this.settings.barColor = $(this)
              .val();
              _this.setInvalid(true);
              _this.repaint();
            });
          phantasus.FormBuilder.showInModal({
            title: 'Bar Color',
            close: 'Close',
            html: formBuilder.$form,
            focus: heatmap.getFocusEl()
          });
        } else if (item === COLOR_BAR_SIZE) {
          var formBuilder = new phantasus.FormBuilder();
          formBuilder.append({
            name: 'size',
            type: 'text',
            value: _this.settings.colorBarSize,
            required: true,
            style: 'max-width:50px;'
          });
          formBuilder.find('size').on(
            'change',
            function () {
              var val = parseFloat($(this)
              .val());
              if (val > 0) {
                _this.settings.colorBarSize = val;
                _this.setInvalid(true);
                _this.repaint();
              }
            });
          phantasus.FormBuilder.showInModal({
            title: 'Color Bar Size',
            close: 'Close',
            html: formBuilder.$form,
            focus: heatmap.getFocusEl()
          });
        } else if (item === ANNOTATE_SELECTION) {
          heatmap.getActionManager().execute(isColumns ? 'Annotate Selected Columns' : 'Annotate' +
            ' Selected Rows');
        } else if (item === DELETE) {
          phantasus.FormBuilder
          .showOkCancel({
            title: 'Delete',
            content: 'Are you sure you want to delete '
            + _this.name + '?',
            okCallback: function () {
              var metadata = isColumns ? project
                .getFullDataset()
                .getColumnMetadata()
                : project
                .getFullDataset()
                .getRowMetadata();
              metadata
              .remove(phantasus.MetadataUtil
              .indexOf(
                metadata,
                _this.name));
              var sortKeys = isColumns ? project
                .getColumnSortKeys()
                : project
                .getRowSortKeys();
              var sortKeyIndex = _.indexOf(
                sortKeys.map(function (key) {
                  return key.field;
                }), _this.name);
              if (sortKeyIndex !== -1) {
                sortKeys.splice(
                  sortKeyIndex, 1);
                if (isColumns) {
                  project
                  .setColumnSortKeys(
                    sortKeys,
                    true);
                } else {
                  project.setRowSortKeys(
                    sortKeys, true);
                }
              }
              var groupByKeys = isColumns ? project
                .getGroupColumns()
                : project
                .getGroupRows();
              var groupByKeyIndex = _
              .indexOf(
                groupByKeys
                .map(function (key) {
                  return key.field;
                }),
                _this.name);
              if (groupByKeyIndex !== -1) {
                groupByKeys.splice(
                  groupByKeyIndex, 1);
                if (isColumns) {
                  project
                  .setGroupColumns(
                    groupByKeys,
                    true);
                } else {
                  project.setGroupRows(
                    groupByKeys,
                    true);
                }
              }
              if (!isColumns) {
                // remove from any group
                // by or sort by
                project
                .trigger(
                  'rowTrackRemoved',
                  {
                    vector: _this
                    .getFullVector()
                  });
              } else {
                project
                .trigger(
                  'columnTrackRemoved',
                  {
                    vector: _this
                    .getFullVector()
                  });
              }
            }
          });
        } else if (item === CLEAR_SELECTION) {
          heatmap.getActionManager().execute(isColumns ? 'Clear Selected Columns' : 'Clear' +
            ' Selected Rows');
        } else if (item === INVERT_SELECTION) {
          heatmap.getActionManager().execute(isColumns ? 'Invert Selected Columns' : 'Invert' +
            ' Selected Rows');
        } else if (item === MOVE_TO_TOP) {
          heatmap.getActionManager().execute(isColumns ? 'Move Selected Columns To Top' : 'Move' +
            ' Selected Rows To Top');
        } else if (item === SORT_ASC || item === SORT_DESC) {
          var sortKey = new phantasus.SortKey(
            _this.name,
            item === SORT_ASC ? phantasus.SortKey.SortOrder.ASCENDING
              : phantasus.SortKey.SortOrder.DESCENDING);
          if (_this.isColumns) {
            _this.project
            .setColumnSortKeys(
              phantasus.SortKey
              .keepExistingSortKeys(
                [sortKey],
                project
                .getColumnSortKeys()),
              true);
          } else {
            _this.project
            .setRowSortKeys(
              phantasus.SortKey
              .keepExistingSortKeys(
                [sortKey],
                project
                .getRowSortKeys()),
              true);
          }
        } else if (item == SORT_SEL_ASC
          || item == SORT_SEL_DESC
          || item === SORT_SEL_TOP_N) {
          var sortOrder;
          if (item === SORT_SEL_ASC) {
            sortOrder = phantasus.SortKey.SortOrder.ASCENDING;
          } else if (item === SORT_SEL_DESC) {
            sortOrder = phantasus.SortKey.SortOrder.DESCENDING;
          } else {
            sortOrder = phantasus.SortKey.SortOrder.TOP_N;
          }
          heatmap.sortBasedOnSelection(sortOrder,
            isColumns, e && e.shiftKey);
        } else if (item === SELECT_ALL) {
          heatmap.getActionManager().execute(isColumns ? 'Select All Columns' : 'Select All Rows');
        } else if (item === 'Auto Range') {
          delete _this.settings.min;
          delete _this.settings.max;
          delete _this.settings.mid;
          _this._update();
          heatmap.revalidate();
        } else if (item === 'Custom Range...') {
          var formBuilder = new phantasus.FormBuilder();
          var items = [
            {name: 'min',
            required: true,
            type: 'number',
            value: _this.settings.min
          }, {
            name: 'mid',
            required: true,
            type: 'number',
            value: _this.settings.mid
          }, {
            name: 'max',
            required: true,
            type: 'number',
            value: _this.settings.max
          }];
          _.each(items, function (item) {
            formBuilder.append(item);
          });
          phantasus.FormBuilder
          .showOkCancel({
            title: 'Range',
            content: formBuilder.$form,
            okCallback: function () {
              _this.settings.min = parseFloat(formBuilder
              .getValue('min'));
              _this.settings.mid = parseFloat(formBuilder
              .getValue('mid'));
              _this.settings.max = parseFloat(formBuilder
              .getValue('max'));
              _this._update();
              heatmap.revalidate();
            }
          });
        } else if (item === 'Squished') {
          _this.settings.squished = !_this.settings.squished;
          heatmap.revalidate();
        } else if (item === 'Color Key') {

          var legend = new phantasus.HeatMapTrackColorLegend(
            [_this], isColumns ? _this.project
              .getColumnColorModel()
              : _this.project
              .getRowColorModel());
          var size = legend.getPreferredSize();
          legend.setBounds(size);
          legend.repaint();

          phantasus.FormBuilder.showInModal({
            title: 'Color Key',
            html: legend.canvas,
            focus: heatmap.getFocusEl()
          });
        } else if (item === 'Shape Key') {
          var legend = new phantasus.HeatMapTrackShapeLegend(
            [_this], isColumns ? _this.project
              .getColumnShapeModel()
              : _this.project
              .getRowShapeModel());
          var size = legend.getPreferredSize();
          legend.setBounds(size);
          legend.repaint();

            phantasus.FormBuilder.showInModal({
              title: 'Shape Key',
              html: legend.canvas,
              focus: heatmap.getFocusEl()
            });
          } else if (item === 'Font Key') {
            var legend = new phantasus.HeatMapTrackFontLegend(
              [_this], isColumns ? _this.project
                  .getColumnFontModel()
                : _this.project
                  .getRowFontModel());
            var size = legend.getPreferredSize();
            legend.setBounds(size);
            legend.repaint();
            phantasus.FormBuilder.showInModal({
              title: 'Font Key',
              html: legend.canvas,
              focus: heatmap.getFocusEl()
            });
          } else if (item === 'Edit Fonts...') {
            heatmap.getActionManager().execute('Edit Fonts');
          } else if (item === 'Edit Shapes...') {
            var shapeFormBuilder = new phantasus.FormBuilder();
            var shapeModel = isColumns ? _this.project
              .getColumnShapeModel() : _this.project
              .getRowShapeModel();
            var chooser = new phantasus.ShapeChooser({
              map: shapeModel.getMap(_this.name)
            });

            chooser.on('change', function (event) {
              shapeModel.setMappedValue(_this
                  .getFullVector(), event.value,
                event.shape);
              _this.setInvalid(true);
              _this.repaint();
            });
            phantasus.FormBuilder.showInModal({
              title: 'Edit Shapes',
              html: chooser.$div,
              close: 'Close',
              focus: heatmap.getFocusEl()
            });
          } else if (item === 'Edit Colors...') {
            var colorModel = isColumns ? _this.project
              .getColumnColorModel() : _this.project
              .getRowColorModel();
            var colorSchemeChooser = new phantasus.ColorSchemeChooser({track: _this, heatMap: _this.heatmap, colorModel: colorModel});
            phantasus.FormBuilder.showInModal({
              title: 'Edit Colors',
              html: colorSchemeChooser.$div,
              close: 'Close',
              focus: heatmap.getFocusEl()
            });
          } else if (item === TOOLTIP) {
            _this.settings.inlineTooltip = !_this.settings.inlineTooltip;
          } else if (item === HIGHLIGHT_MATCHING_VALUES) {
            _this.settings.highlightMatchingValues = !_this.settings.highlightMatchingValues;
          } else if ((customItem = _
              .find(
                customItems,
                function (customItem) {
                  return customItem.name === item
                    && customItem.columns === isColumns;
                }))) {
            if (customItem.task) {
              // add task
              var task = {
                tabId: _this.heatmap.getTabManager()
                  .getActiveTabId()
              };

              _this.heatmap.getTabManager().addTask(task);
              setTimeout(function () {
                customItem.callback(heatmap);
                _this.heatmap.getTabManager()
                  .removeTask(task);
              }, 1);
            } else {
              customItem.callback(heatmap);
            }

          } else if (item === DISPLAY_CONTINUOUS) {
            var discrete = _this.getFullVector().getProperties().get(phantasus.VectorKeys.DISCRETE) || false;
            _this.getFullVector().getProperties().set(phantasus.VectorKeys.DISCRETE, !discrete);
            _this._setChartMinMax();
            _this.setInvalid(true);
            _this.repaint();
          } else if (item === HIDE) {
            heatmap.setTrackVisible(_this.name, false,
              _this.isColumns);
            heatmap.revalidate();
          } else if (item === HIDE_OTHERS) {
            var names = heatmap.getVisibleTrackNames(_this.isColumns);
            for (var i = 0; i < names.length; i++) {
              if (names[i] !== _this.name) {
                heatmap.setTrackVisible(names[i], false,
                  _this.isColumns);
              }
            }

            heatmap.revalidate();

          } else if (item === DISPLAY_STACKED_BAR) {
            _this.settings.stackedBar = !_this.settings.stackedBar;
            _this._update();
            heatmap.revalidate();
          } else {
            if (item === DISPLAY_BAR) {
              item = phantasus.VectorTrack.RENDER.BAR;
            } else if (item === DISPLAY_COLOR) {
              item = phantasus.VectorTrack.RENDER.COLOR;
            } else if (item === DISPLAY_TEXT) {
              item = phantasus.VectorTrack.RENDER.TEXT;
            } else if (item === DISPLAY_TEXT_AND_COLOR) {
              item = phantasus.VectorTrack.RENDER.TEXT_AND_COLOR;
            } else if (item === DISPLAY_TEXT_AND_FONT) {
              item = phantasus.VectorTrack.RENDER.TEXT_AND_FONT;
            } else if (item === DISPLAY_STRUCTURE) {
              item = phantasus.VectorTrack.RENDER.MOLECULE;
            } else if (item === DISPLAY_SHAPE) {
              item = phantasus.VectorTrack.RENDER.SHAPE;
            } else if (item === DISPLAY_ARC) {
              item = phantasus.VectorTrack.RENDER.ARC;
            } else if (item === DISPLAY_BOX_PLOT) {
              item = phantasus.VectorTrack.RENDER.BOX_PLOT;
            } else {
              console.log('Unknown item ' + item);
            }
            var show = !_this.isRenderAs(item);
            var remove;
            if (!show) {
              _this.settings.display.splice(_this.settings.display.indexOf(item), 1);
              // if no longer rendering as text, remove TEXT_AND_COLOR and TEXT_AND_FONT
              if (item === phantasus.VectorTrack.RENDER.TEXT) {
                remove = [phantasus.VectorTrack.RENDER.TEXT_AND_FONT, phantasus.VectorTrack.RENDER.TEXT_AND_COLOR];
              }
            } else {
              if (item === phantasus.VectorTrack.RENDER.COLOR) {
                remove = [phantasus.VectorTrack.RENDER.TEXT_AND_COLOR];
              } else if (item === phantasus.VectorTrack.RENDER.TEXT_AND_COLOR) {
                remove = [phantasus.VectorTrack.RENDER.COLOR];
              }
              _this.settings.display.push(item);
            }
            if (remove) {
              remove.forEach(function (key) {
                var index = _this.settings.display.indexOf(key);
                if (index !== -1) {
                  _this.settings.display.splice(index, 1);
                }
              });
            }
            _this._update();
            heatmap.revalidate();
            if (show && item === phantasus.VectorTrack.RENDER.TEXT_AND_FONT) {
              heatmap.getActionManager().execute('Edit Fonts');
            }
          }
        });
  }
  ,
  renderColor: function (context, vector, start, end, clip, offset, continuous) {
    var isColumns = this.isColumns;
    var positions = this.positions;
    var colorModel = isColumns ? this.project.getColumnColorModel() : this.project.getRowColorModel();
    var settings = this.settings;
    var canvasSize = isColumns ? this.getUnscaledHeight() : this.getUnscaledWidth();
    var colorBarSize = settings.colorBarSize;
    if (colorBarSize > canvasSize) {
      colorBarSize = canvasSize >= 5 ? (canvasSize - 1)
        : canvasSize;
    }
    var getColor;
    if (!continuous) {
      getColor = _.bind(colorModel.getMappedValue, colorModel);
    } else {
      getColor = _.bind(colorModel.getContinuousMappedValue, colorModel);
    }

    if (vector.getProperties().get(
        phantasus.VectorKeys.FIELDS) != null) {
      var visibleFieldIndices = vector.getProperties().get(
        phantasus.VectorKeys.VISIBLE_FIELDS);
      if (visibleFieldIndices == null) {
        visibleFieldIndices = phantasus.Util.seq(vector.getProperties().get(
          phantasus.VectorKeys.FIELDS).length);
      }
      colorBarSize /= visibleFieldIndices.length;
      var nvisibleFieldIndices = visibleFieldIndices.length;

      for (var i = start; i < end; i++) {
        var array = vector.getValue(i);
        var position = positions.getPosition(i);
        var size = positions.getItemSize(i);
        var _offset = offset;
        if (array != null) {
          for (var j = 0; j < nvisibleFieldIndices; j++) {
            var value = array[visibleFieldIndices[j]];
            var color = getColor(vector, value);
            context.fillStyle = color;
            if (isColumns) {
              context.beginPath();
              context.rect(position, _offset - colorBarSize, size,
                colorBarSize);
              context.fill();
            } else {
              context.beginPath();
              context.rect(_offset, position, colorBarSize, size);
              context.fill();
            }
            _offset += colorBarSize;
          }
        }

      }

    } else {
      for (var i = start; i < end; i++) {
        var value = vector.getValue(i);
        var position = positions.getPosition(i);
        var size = positions.getItemSize(i);
        var color = getColor(vector, value);
        context.fillStyle = color;
        if (isColumns) {
          context.beginPath();
          context.rect(position, offset - colorBarSize, size,
            settings.colorBarSize);
          context.fill();
        } else {
          context.beginPath();
          context.rect(offset, position, colorBarSize, size);
          context.fill();
        }
      }
    }
  }
  ,
  renderShape: function (context, vector, start, end, clip, offset) {
    var isColumns = this.isColumns;
    var positions = this.positions;
    var shapeModel = isColumns ? this.project.getColumnShapeModel()
      : this.project.getRowShapeModel();
    var settings = this.settings;
    var canvasSize = isColumns ? this.getUnscaledHeight() : this
      .getUnscaledWidth();
    var colorBarSize = settings.colorBarSize;
    if (colorBarSize > canvasSize) {
      colorBarSize = canvasSize >= 5 ? (canvasSize - 1)
        : canvasSize;
    }
    context.fillStyle = 'black';
    context.strokeStyle = 'black';

    var lineWidth = context.lineWidth;
    context.lineWidth = 1;
    for (var i = start; i < end; i++) {

      var value = vector.getValue(i);
      var position = positions.getPosition(i);
      var itemSize = positions.getItemSize(i);
      var minSize = Math.min(colorBarSize, itemSize);
      var size2 = minSize / 2;
      var shape = shapeModel.getMappedValue(vector, value);
      // x and y are at center
      var x = isColumns ? position + itemSize / 2 : offset + size2;
      var y = isColumns ? offset - size2 : position + itemSize / 2;
      size2 -= 0.5; // small border between cells
      phantasus.CanvasUtil.drawShape(context, shape, x, y, size2);
    }
    context.lineWidth = lineWidth;
  }
  ,
  renderHeatMap: function (context, vector, start, end, clip, size) {
    var isColumns = this.isColumns;
    var positions = this.positions;
    var project = this.project;
    context.save();
    context.lineWidth = 1;
    //	context.translate(clip.x, clip.y);
    var width = clip.width;
    var height = clip.height;
    var colorScheme = this.heatmap.getHeatMapElementComponent().getColorScheme();
    var drawGrid = this.heatmap.getHeatMapElementComponent().isDrawGrid();
    var gridColor = this.heatmap.getHeatMapElementComponent().getGridColor();
    var gridThickness = this.heatmap.getHeatMapElementComponent().getGridThickness();
    for (var i = start; i < end; i++) {
      var value = vector.getValue(i); // value is an array of values to display as a heat map
      if (value != null) {
        var pix = positions.getPosition(i);
        var itemSize = positions.getItemSize(i);
        var currentPix = 0;
        var nvalues = value.length;
        var pixPer = size / nvalues;
        for (var j = 0; j < nvalues; j++) {
          var val = value[j];
          context.fillStyle = colorScheme.getColor(i, -1, val);
          context.fillRect(j * pixPer, pix, pixPer, itemSize);
        }
        if (drawGrid && itemSize > 10) {
          context.strokeStyle = gridColor;
          context.lineWidth = gridThickness;
          context.beginPath();
          for (var j = 0; j < nvalues; j++) {
            var val = value[j];
            context.rect(j * pixPer, pix, pixPer, itemSize);
          }
          context.stroke();

        }
      }

    }

    context.restore();
  }
  ,
  renderArc: function (context, vector, start, end, clip, size) {

    var isColumns = this.isColumns;
    var positions = this.positions;
    var project = this.project;
    context.save();
    context.lineWidth = 1;
    // var scale = d3.scale.linear().domain([1, size]).range([0.8, 1])
    // .clamp(true);
    // var fill = d3.scale.category20b();

    var total = positions.getPosition(positions.getLength() - 1)
      + positions.getItemSize(positions.getLength() - 1);
    context.translate(clip.x, clip.y);
    var width = clip.width;
    var height = clip.height;
    // if (!isColumns) {
    // 	var squishFactor = height / total;
    // 	context.scale(1, squishFactor);
    // } else {
    // 	var squishFactor = width / total;
    // 	context.scale(squishFactor, 1);
    // }

    for (var i = start; i < end; i++) {
      var value = vector.getValue(i); // value is an array of other indices to link to
      if (value != null) {
        var startPix = positions.getPosition(i) + positions.getItemSize(i)
          / 2;
        for (var j = 0, nindices = value.length; j < nindices; j++) {
          var viewIndex = value[j];
          var endPix = positions.getPosition(viewIndex)
            + positions.getItemSize(viewIndex) / 2;
          var midPix = (endPix + startPix) / 2;
          var distance = Math.abs(i - viewIndex);
          var arcRadius = size; // scale(distance) * size;
          if (isColumns) {
            context.beginPath();
            context.moveTo(startPix, arcRadius);
            context.quadraticCurveTo(midPix, 1, endPix, arcRadius);
          } else {
            // console.log(i, viewIndex, startPix, endPix);
            context.beginPath();
            context.moveTo(1, startPix);
            context.quadraticCurveTo(arcRadius, midPix, 1, endPix);
          }

          context.stroke();

        }
      }

    }
    context.restore();
  }
  ,
  sdfToSvg: function (sdf, width, height) {
    if (!this.jsme && typeof JSApplet !== 'undefined') {
      this.jsmeId = _.uniqueId('m');
      this.$jsmeDiv = $(
        '<div id="'
        + this.jsmeId
        + '" style="position:absolute;left:-10000px;top:-10000px;"></div>')
        .appendTo($(document.body));
      this.jsme = new JSApplet.JSME(this.jsmeId, '380px', '340px', {});
    }
    // this.$jsmeDiv.css('width', width + 'px').css('height', height +
    // 'px');
    // this.jsme.setSize(width + 'px', height + 'px');
    this.jsme.readMolFile(sdf);
    var svg = $('#' + this.jsmeId + ' > div > div > div:nth-child(2) > svg');
    var svgWidth = svg.width.baseVal.value;
    var svgHeight = svg.height.baseVal.value;
    var scale = Math.min(width / svgWidth, height / svgHeight);
    var text = '<svg><g transform="scale(' + scale + ')">' + svg.innerHTML
      + '</g></svg>';
    return text;
  }
  ,
  renderMolecule: function (context, vector, start, end, clip, offset) {
    var isColumns = this.isColumns;
    var positions = this.positions;
    context.fillStyle = phantasus.CanvasUtil.FONT_COLOR;
    context.strokeStyle = 'black';
    var width = this.getUnscaledWidth();
    var customUrlProvider = this.heatmap.options.structureUrlProvider !== undefined;
    var dummyTarget = {
      childNodes: [],
      getContext: function () {
        return context;
      }
    };
    for (var i = start; i < end; i++) {
      var spanEnd = this.spanMap.get(i);
      if (spanEnd !== undefined) {
        var startPix = positions.getPosition(i);
        var endPix = positions.getPosition(spanEnd - 1)
          + positions.getSize();
        var size = endPix - startPix;
        var value = vector.getValue(i);
        var cache = this.moleculeCache[value];
        if (cache) {
          if (customUrlProvider) {
            if (cache.complete) {
              // 800 x 400
              var scaleFactor = Math.min(size / cache.height,
                width / cache.width);
              var scaledWidth = cache.width * scaleFactor;
              var scaledHeight = cache.height * scaleFactor;
              var diff = cache.height - scaledHeight;
              startPix += diff / 2;
              try {
                context.drawImage(cache, offset, startPix,
                  scaledWidth, scaledHeight);
              }
              catch (x) {

              }
            }
          } else {
            var text = this.sdfToSvg(cache, width, size);
            canvg(dummyTarget, text, {
              ignoreMouse: true,
              ignoreAnimation: true,
              offsetY: startPix,
              ignoreClear: true,
              ignoreDimensions: true
            });
          }
        }
      }
    }
  }
  ,
  createChartScale: function (availableSpace) {
    var domain;
    var range;
    if (this.settings.mid !== this.settings.min
      && this.settings.mid !== this.settings.max) {
      domain = [this.settings.min, this.settings.mid, this.settings.max];
      range = this.isColumns ? [availableSpace, availableSpace / 2, 0]
        : [0, availableSpace / 2, availableSpace];
    } else {
      domain = [this.settings.min, this.settings.max];
      range = this.isColumns ? [availableSpace, 0] : [
        0,
        availableSpace];
    }
    var scale = d3.scale.linear().domain(domain).range(range).clamp(true);
    return scale;
  }
  ,
  renderBar: function (context, vector, start, end, clip, offset, availableSpace) {
    var isColumns = this.isColumns;
    var positions = this.positions;
    context.fillStyle = this.settings.barColor;
    var scale = this.createChartScale(availableSpace);
    var midPix = scale(this.settings.mid);
    var settings = this.settings;
    var discrete = vector.getProperties().get(phantasus.VectorKeys.DISCRETE) && this.discreteValueMap != null;
    var colorByVector = this.settings.colorByField != null ? this
      .getVector(this.settings.colorByField) : null;
    var colorModel = isColumns ? this.project.getColumnColorModel()
      : this.project.getRowColorModel();

    for (var i = start; i < end; i++) {
      var value = vector.getValue(i);
      if (discrete) {
        value = this.discreteValueMap.get(value);
      }
      var position = positions.getPosition(i);
      var size = positions.getItemSize(i);
      var scaledValue = scale(value);
      if (colorByVector !== null) {
        context.fillStyle = colorModel.getMappedValue(colorByVector, colorByVector.getValue(i));
      }
      if (isColumns) {
        context.beginPath();
        context.rect(position, Math.min(midPix, scaledValue) + offset, size,
          Math.abs(midPix - scaledValue));
        context.fill();
      } else {
        context.beginPath();
        context.rect(offset + Math.min(midPix, scaledValue), position,
          Math.abs(midPix - scaledValue), size);
        context.fill();
      }
    }
  }
  ,
  renderBoxPlot: function (context, vector, start, end, clip, offset, availableSpace) {
    var isColumns = this.isColumns;
    var positions = this.positions;
    context.strokeStyle = 'black';
    context.save();
    context.translate(offset, 0);
    var scale = this.createChartScale(availableSpace);
    var visibleFieldIndices = vector.getProperties().get(
      phantasus.VectorKeys.VISIBLE_FIELDS);

    var colorByVector = this.settings.colorByField != null ? this
      .getVector(this.settings.colorByField) : null;
    var colorModel = isColumns ? this.project.getColumnColorModel()
      : this.project.getRowColorModel();
    for (var i = start; i < end; i++) {
      var array = vector.getValue(i);
      if (array != null) {
        var itemSize = positions.getItemSize(i);
        if (itemSize <= 3) {
          continue;
        }
        var radius = 2;
        var pix = positions.getPosition(i);
        var start = pix + 1;
        var end = pix + itemSize - 1;
        var center = (start + end) / 2;
        var _itemSize = itemSize - 2;
        var lineHeight = Math.max(2, _itemSize - 8);
        var box = array.summary;
        if (box == null) {
          var v = phantasus.VectorUtil.arrayAsVector(array);
          box = phantasus
            .BoxPlotItem(visibleFieldIndices != null ? new phantasus.SlicedVector(
              v, visibleFieldIndices)
              : v);
          array.summary = box;
        }
        context.fillStyle = '#bdbdbd';

        if (!isColumns) {
          // box from q1 (25th q) to q3
          context.fillRect(Math.min(scale(box.q1), scale(box.q3)),
            start, Math.abs(scale(box.q1) - scale(box.q3)),
            _itemSize);
          // draw line from q1 to lav
          context.fillRect(Math.min(scale(box.q1),
            scale(box.lowerAdjacentValue)), center - lineHeight
            / 2, Math.abs(scale(box.q1)
            - scale(box.lowerAdjacentValue)), lineHeight);
          // draw line from q3 to uav
          context.fillRect(Math.min(scale(box.q3),
            scale(box.upperAdjacentValue)), center - lineHeight
            / 2, Math.abs(scale(box.q3)
            - scale(box.upperAdjacentValue)), lineHeight);
          context.fillStyle = '#31a354';
          // highlight median
          context.fillRect(scale(box.median) - 3, start, 3, end - start);
          context.fillStyle = '#636363';

          // draw individual points
          // for (var j = 0, length = visibleFieldIndices == null ? array.length : visibleFieldIndices.length; j < length; j++) {
          // 	var index = visibleFieldIndices == null ? j : visibleFieldIndices[j];
          // 	var value = array[index];
          // 	if (value != null) {
          // 		if (colorByVector != null) {
          // 			var colorByArray = colorByVector.getValue(i);
          // 			if (colorByArray != null) {
          // 				var color = colorModel
          // 				.getMappedValue(
          // 					colorByVector,
          // 					colorByArray[index]);
          // 				context.fillStyle = color;
          // 			} else {
          // 				context.fillStyle = '#636363';
          // 			}
          //
          // 		}
          // 		var pix = scale(value);
          // 		context.beginPath();
          // 		context
          // 		.arc(pix, center, radius, Math.PI * 2,
          // 			false);
          // 		context.fill();
          // 	}
          // }

        } else { // TOD implement for columns

        }

      }
    }
    context.restore();
  }
  ,
  renderStackedBar: function (context, vector, start, end, clip, offset, availableSpace) {
    var isColumns = this.isColumns;
    var positions = this.positions;
    var scale = this.createChartScale(availableSpace);
    var midPix = scale(this.settings.mid);
    var settings = this.settings;
    var colorModel = isColumns ? this.project.getColumnColorModel()
      : this.project.getRowColorModel();
    context.strokeStyle = 'black';
    context.lineWidth = 2;
    for (var i = start; i < end; i++) {
      var array = vector.getValue(i);
      if (array != null) {
        var position = positions.getPosition(i);
        var size = positions.getItemSize(i);
        var positivePairs = [];
        var negativePairs = [];
        for (var j = 0, length = array.length; j < length; j++) {
          var value = array[j];
          if (value >= this.settings.mid) {
            positivePairs.push({
              value: value,
              index: j
            });
          } else if (value < 0) {
            negativePairs.push({
              value: value,
              index: j
            });
          }
        }

        // array.sort(function (a, b) {
        // 	return (a.value < b.value ? 1 : (a.value === b.value ? 0 : -1));
        // });
        // var positiveIndices = [];
        // positivePairs.forEach(function (item) {
        // 	positiveIndices.push(item.index);
        // });
        //
        var positiveIndices = phantasus.Util.indexSortPairs(
          positivePairs, false);
        for (var j = 0, length = positiveIndices.length; j < length; j++) {
          var index = positiveIndices[j];
          var value = array[index];
          var color = colorModel.getMappedValue(vector, index);
          context.fillStyle = color;
          var scaledValue = scale(value);
          var nextScaledValue = j === (length - 1) ? midPix
            : scale(array[positiveIndices[j + 1]]);
          if (isColumns) {
            context.beginPath();
            context.rect(position, Math.min(nextScaledValue,
              scaledValue), size, Math.abs(nextScaledValue
              - scaledValue));
            context.fill();
          } else {
            context.beginPath();
            context.rect(offset
              + Math.min(nextScaledValue, scaledValue),
              position, Math.abs(nextScaledValue
                - scaledValue), size);
            context.fill();
          }
        }
        var negativeIndices = phantasus.Util.indexSortPairs(
          negativePairs, true); // draw smaller (more negative)
        // values 1st
        for (var j = 0, length = negativeIndices.length; j < length; j++) {
          var index = negativeIndices[j];
          var value = array[index];
          var color = colorModel.getMappedValue(vector, index);
          context.fillStyle = color;
          var scaledValue = scale(value);
          var nextScaledValue = j === (length - 1) ? midPix
            : scale(array[negativeIndices[j + 1]]);
          if (isColumns) {
            context.beginPath();
            context.rect(position, Math.min(nextScaledValue,
              scaledValue), size, Math.abs(nextScaledValue
              - scaledValue));
            context.fill();
          } else {
            context.beginPath();
            context.rect(offset
              + Math.min(nextScaledValue, scaledValue),
              position, Math.abs(nextScaledValue
                - scaledValue), size);
            context.fill();
          }

        }
      }
    }
    context.lineWidth = 1;
  }
  ,
  renderUnstackedBar: function (context, vector, start, end, clip, offset, availableSpace, fieldIndices) {
    var isColumns = this.isColumns;
    var positions = this.positions;
    var nvalues = fieldIndices.length;
    var settings = this.settings;
    var colorModel = isColumns ? this.project.getColumnColorModel()
      : this.project.getRowColorModel();
    context.fillStyle = this.settings.barColor;
    // context.strokeStyle = '0000f0';
    var barSpacer = 0;
    var barWidth = (availableSpace - (nvalues - 1) * barSpacer) / nvalues;
    var colorByVector = this.settings.colorByField != null ? this
      .getVector(this.settings.colorByField) : null;
    context.strokeStyle = 'white';
    for (var i = start; i < end; i++) {
      var array = vector.getValue(i);
      if (array != null) {
        var position = positions.getPosition(i);
        var itemSize = positions.getItemSize(i);
        var scale = this.createChartScale(itemSize - 1);
        var midPix = scale(this.settings.mid); // need to set mid pix
        // for each item
        var xpix = 0;
        for (var j = 0; j < nvalues; j++) {
          var value = array[fieldIndices[j]];
          if (colorByVector != null) {
            var colorByArray = colorByVector.getValue(i);
            var color = colorModel
              .getMappedValue(
                colorByVector,
                colorByArray != null ? colorByArray[fieldIndices[j]]
                  : null);
            context.fillStyle = color;
          }

          var scaledValue = scale(value);

          if (isColumns) {
            context.beginPath();
            context.rect(Math.min(midPix, scaledValue), offset
              + xpix, Math.abs(midPix - scaledValue),
              barWidth);
            context.fill();
          } else {
            // bar always goes to midpix
            context.beginPath();
            var barHeight = Math.abs(midPix - scaledValue);
            var ypix = position + itemSize
              - Math.max(midPix, scaledValue);
            context.rect(offset + xpix, ypix, barWidth, barHeight);
            context.fill();

          }

          xpix += barWidth + barSpacer;
        }

      }

    }
  }
  ,
  renderText: function (context, vector, isColor, isFont, start, end, clip, offset,
                        canvasSize) {

    context.textBaseline = 'middle';
    if (typeof C2S !== 'undefined' && context instanceof C2S) { // FIXME hack for svg text alignment
      context.translate(this.isColumns ? 2 : 0, this.isColumns ? 0 : 2);
    }
    var positions = this.positions;
    var isColumns = this.isColumns;

    var colorModel = isColumns ? this.project.getColumnColorModel()
      : this.project.getRowColorModel();

    var fontModel = isColumns ? this.project.getColumnFontModel()
      : this.project.getRowFontModel();

    if (isColumns) {
      context.translate(clip.x, clip.y); // reset transform, needed for export to svg
    }
    var colorByVector = this.settings.colorByField != null ? this
      .getVector(this.settings.colorByField) : vector;
    var getColor;
    if (colorByVector.getProperties().get(phantasus.VectorKeys.DISCRETE)) {
      getColor = _.bind(colorModel.getMappedValue, colorModel);
    } else {
      getColor = _.bind(colorModel.getContinuousMappedValue, colorModel);
    }
    var fontVector = this.settings.fontField != null ? this
      .getVector(this.settings.fontField) : vector;
    var toStringFunction = phantasus.VectorTrack.vectorToString(vector);
    var font = context.font;
    for (var i = start; i < end; i++) {
      var size = this.positions.getItemSize(i);
      if (size < 6) {
        continue;
      }
      var value = vector.getValue(i);
      if (value != null) {
        var stringValue = toStringFunction(value);
        var position = positions.getPosition(i);
        if (isColor) {
          context.fillStyle = getColor(colorByVector, colorByVector.getValue(i));
        }
        if (isFont) {
          context.font = fontModel.getMappedValue(fontVector, fontVector.getValue(i)).weight + ' ' + font;
        }
        if (isColumns) {
          context.save();
          context.translate(position + size / 2 - clip.x,
            offset - clip.y);
          context.rotate(-Math.PI / 2);
          context.fillText(stringValue, 0, 0);
          context.restore();
        } else {
          context.fillText(stringValue, offset, position + size / 2);
        }
      }
    }
    context.font = font;
  }
};
phantasus.Util.extend(phantasus.VectorTrack, phantasus.AbstractCanvas);
phantasus.VectorTrack.MAX_FONT_SIZE = 18;

/**
 * Performs clustering using pairwise average linking on the given distance
 * matrix.
 *
 * @return array of nodes. Each node object contains a left, right, and
 *         distance.
 */
phantasus.AverageLinkage = function (nelements, distmatrix) {
  var j;
  var n;
  var clusterid;
  var number;
  var result;
  clusterid = []; // nelements;
  number = []; // nelements;
  result = []; // nelements - 1;
  for (var i = 0; i < nelements - 1; i++) {
    result[i] = {
      left: 0,
      right: 0,
      distance: 0
    };
  }
  /*
   * Setup a list specifying to which cluster an element belongs, and keep
   * track of the number of elements in each cluster (needed to calculate the
   * average).
   */
  for (j = 0; j < nelements; j++) {
    number[j] = 1;
    clusterid[j] = j;
  }
  // ip, jp, and distance;
  var r = {};
  // result array contains array of int left, int right, float distance;
  for (n = nelements; n > 1; n--) {
    phantasus.HCluster.findClosestPair(n, distmatrix, r);
    result[nelements - n] = {};
    result[nelements - n].distance = r.distance;
    var is = r.ip;
    var js = r.jp;
    /* Save result */
    result[nelements - n].left = clusterid[is];
    result[nelements - n].right = clusterid[js];
    /* Fix the distances */
    var sum = number[is] + number[js];
    for (j = 0; j < js; j++) {
      distmatrix[js][j] = distmatrix[is][j] * number[is]
        + distmatrix[js][j] * number[js];
      distmatrix[js][j] /= sum;
    }
    for (j = js + 1; j < is; j++) {
      distmatrix[j][js] = distmatrix[is][j] * number[is]
        + distmatrix[j][js] * number[js];
      distmatrix[j][js] /= sum;
    }
    for (j = is + 1; j < n; j++) {
      distmatrix[j][js] = distmatrix[j][is] * number[is]
        + distmatrix[j][js] * number[js];
      distmatrix[j][js] /= sum;
    }
    for (j = 0; j < is; j++) {
      distmatrix[is][j] = distmatrix[n - 1][j];
    }
    for (j = is + 1; j < n - 1; j++) {
      distmatrix[j][is] = distmatrix[n - 1][j];
    }
    /* Update number of elements in the clusters */
    number[js] = sum;
    number[is] = number[n - 1];
    /* Update clusterids */
    clusterid[js] = n - nelements - 1;
    clusterid[is] = clusterid[n - 1];
  }
  return result;
};

phantasus.CollapseDataset = function (dataset, collapseToFields,
                                     summarizeFunction, shallowCopy) {

  var vectors = [];
  var nfields = collapseToFields.length;

  for (var i = 0; i < nfields; i++) {
    var v = dataset.getRowMetadata().getByName(collapseToFields[i]);
    if (!v) {
      throw collapseToFields[i]
      + ' not found. Available fields are '
      + phantasus.MetadataUtil.getMetadataNames(dataset
        .getRowMetadata());
    }
    vectors.push(v);
  }

  var idToIndices = phantasus.VectorUtil.createValuesToIndicesMap(vectors);

  var collapsedDataset = new phantasus.Dataset({
    name: dataset.getName(),
    rows: idToIndices.size(),
    columns: dataset.getColumnCount(),
    dataType: 'Float32'
  });


  var nseries = dataset.getSeriesCount();
  for (var series = 1; series < nseries; series++) {
    collapsedDataset.addSeries({
      name: dataset.getName(i),
      dataType: 'Float32'
    });
  }
  if (shallowCopy) {
    collapsedDataset.setColumnMetadata(dataset.getColumnMetadata());
  } else {
    phantasus.MetadataUtil.copy(dataset.getColumnMetadata(),
      collapsedDataset.getColumnMetadata());
  }
  var collapseToVectors = [];
  for (var i = 0; i < nfields; i++) {
    collapseToVectors.push(collapsedDataset.getRowMetadata().add(
      collapseToFields[i]));
  }

  var counter = 0;
  idToIndices
  .forEach(function (rowIndices, key) {
    // collapse each column separately
    var slice = phantasus.DatasetUtil.slicedView(dataset,
      rowIndices, null);
    var view = new phantasus.DatasetColumnView(slice);
    for (var series = 0; series < nseries; series++) {
      view.setSeriesIndex(series);
      for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
        view.setIndex(j);
        collapsedDataset.setValue(counter, j,
          summarizeFunction(view), series);
      }
    }
    for (var i = 0; i < nfields; i++) {
      var collapsedToVector = collapseToVectors[i];
      var vector = vectors[i];
      collapsedToVector.setValue(counter, vector
      .getValue(rowIndices[0]));
    }
    counter++;
  });
  if (nfields === 1) {
    var newVector = collapseToVectors[0];
    vectors[0].getProperties().forEach(function (val, key) {
      if (!phantasus.VectorKeys.COPY_IGNORE.has(key)) {
        newVector.properties.set(key, val);
      }
    });
  }
  return collapsedDataset;
};

phantasus.SelectRow = function (dataset, collapseToFields,
                                  summarizeFunction, shallowCopy) {
  var vectors = [];
  var nfields = collapseToFields.length;

  for (var i = 0; i < nfields; i++) {
    var v = dataset.getRowMetadata().getByName(collapseToFields[i]);
    if (!v) {
      throw collapseToFields[i]
      + ' not found. Available fields are '
      + phantasus.MetadataUtil.getMetadataNames(dataset
        .getRowMetadata());
    }
    vectors.push(v);
  }

  var idToIndices = phantasus.VectorUtil.createValuesToIndicesMap(vectors);

  var collapsedDataset = new phantasus.Dataset({
    name: dataset.getName(),
    rows: idToIndices.size(),
    columns: dataset.getColumnCount(),
    dataType: 'Float32'
  });


  var nseries = dataset.getSeriesCount();
  for (var series = 1; series < nseries; series++) {
    collapsedDataset.addSeries({
      name: dataset.getName(i),
      dataType: 'Float32'
    });
  }
  if (shallowCopy) {
    collapsedDataset.setColumnMetadata(dataset.getColumnMetadata());
  } else {
    phantasus.MetadataUtil.copy(dataset.getColumnMetadata(),
      collapsedDataset.getColumnMetadata());
  }
  /*var collapseToVectors = [];
  for (var i = 0; i < nfields; i++) {
    collapseToVectors.push(collapsedDataset.getRowMetadata().add(
      collapseToFields[i]));
  }*/
  var names = phantasus.MetadataUtil.getMetadataNames(dataset.getRowMetadata());
  var collapseToVectors = [];
  var allvectors = [];
  for (var i = 0; i < names.length; i++) {
    var v = dataset.getRowMetadata().getByName(names[i]);
    collapseToVectors.push(collapsedDataset.getRowMetadata().add(names[i]));
    allvectors.push(v);
  }

  var counter = 0;

  idToIndices
    .forEach(function (rowIndices, key) {
      var slice = phantasus.DatasetUtil.slicedView(dataset,
      rowIndices, null);
      var view = new phantasus.DatasetRowView(slice);
      var indexInDataset;
      for (var series = 0; series < nseries; series++) {
        view.setSeriesIndex(series);
        var chosenIndex = summarizeFunction(view).index;
        view.setIndex(chosenIndex);
        indexInDataset = view.dataset.rowIndices[chosenIndex];

        for (var j = 0, ncols = dataset.getColumnCount(); j < ncols; j++) {
          collapsedDataset.setValue(counter, j,
            view.getValue(j), series);
        }
      }
      for (var i = 0; i < names.length; i++) {
        var collapsedToVector = collapseToVectors[i];
        var vector = allvectors[i];
        collapsedToVector.setValue(counter, vector
          .getValue(indexInDataset));
      }
      counter++;
    });
  return collapsedDataset;
};

phantasus.CompleteLinkage = function (nelements, distmatrix) {
  var j;
  var n;
  var clusterid = []; // new var[nelements];
  var result = [];// new Node[nelements - 1];
  for (var i = 0; i < nelements - 1; i++) {
    result[i] = {
      left: 0,
      right: 0,
      distance: 0
    };
  }
  /* Setup a list specifying to which cluster a gene belongs */
  for (j = 0; j < nelements; j++) {
    clusterid[j] = j;
  }
  var r = {};
  for (n = nelements; n > 1; n--) {
    phantasus.HCluster.findClosestPair(n, distmatrix, r);
    result[nelements - n].distance = r.distance;
    var is = r.ip;
    var js = r.jp;
    /* Fix the distances */
    for (j = 0; j < js; j++) {
      distmatrix[js][j] = Math.max(distmatrix[is][j], distmatrix[js][j]);
    }
    for (j = js + 1; j < is; j++) {
      distmatrix[j][js] = Math.max(distmatrix[is][j], distmatrix[j][js]);
    }
    for (j = is + 1; j < n; j++) {
      distmatrix[j][js] = Math.max(distmatrix[j][is], distmatrix[j][js]);
    }
    for (j = 0; j < is; j++) {
      distmatrix[is][j] = distmatrix[n - 1][j];
    }
    for (j = is + 1; j < n - 1; j++) {
      distmatrix[j][is] = distmatrix[n - 1][j];
    }
    /* Update clusterids */
    result[nelements - n].left = clusterid[is];
    result[nelements - n].right = clusterid[js];
    clusterid[js] = n - nelements - 1;
    clusterid[is] = clusterid[n - 1];
  }
  return result;
};

phantasus.HClusterGroupBy = function (dataset, groupByFieldNames, distanceFunction, linkageMethod) {
  var model = dataset.getRowMetadata();
  var vectors = phantasus.MetadataUtil.getVectors(dataset.getRowMetadata(),
    groupByFieldNames);
  var idToIndices = phantasus.VectorUtil.createValuesToIndicesMap(vectors);
  var reorderedIndices = [];
  var offset = 0;
  var root = {
    id: -1,
    children: [],
    height: 0
  };
  var tree = {
    maxHeight: 0,
    rootNode: root,
    leafNodes: [],
    nLeafNodes: 0
  };
  idToIndices
    .forEach(function (rowIndices, id) {
      var originalIndicesForGroup = idToIndices.get(id);
      var subset = phantasus.DatasetUtil.slicedView(dataset,
        originalIndicesForGroup, null);
      var hcl;
      var distanceMatrix = phantasus.HCluster.computeDistanceMatrix(
        subset, distanceFunction);
      hcl = new phantasus.HCluster(distanceMatrix, linkageMethod);
      var reorderedGroupIndices = hcl.reorderedIndices;
      for (var i = 0, rows = subset.getRowCount(); i < rows; i++) {
        var originalIndex = originalIndicesForGroup[reorderedGroupIndices[i]];
        reorderedIndices.push(originalIndex);
      }

      phantasus.DendrogramUtil.dfs(hcl.tree.rootNode, function (node) {
        node.index += offset;
        node.minIndex += offset;
        node.maxIndex += offset;
        node.id += offset;
        return true;
      });
      if (hcl.tree.leafNodes.length === 0) {
        tree.leafNodes = tree.leafNodes
          .concat([hcl.tree.rootNode]);
      } else {
        tree.leafNodes = tree.leafNodes.concat(hcl.tree.leafNodes);

      }

      root.children.push(hcl.tree.rootNode);
      if (!isNaN(hcl.tree.maxHeight)) {
        tree.maxHeight = Math.max(tree.maxHeight,
          hcl.tree.maxHeight);
      }
      offset += subset.getRowCount();
    });
  tree.nLeafNodes = tree.leafNodes.length;
  tree.rootNode.height = tree.maxHeight;
  this.tree = tree;
  this.reorderedIndices = reorderedIndices;
};

/**
 *
 * @param distmatrix
 * @param linkageAlgorithm {Function}
 * @constructor
 */
phantasus.HCluster = function (distmatrix, linkageAlgorithm) {
  var nelements = distmatrix.length;
  var nNodes = nelements - 1;
  if (nNodes === -1) {

    var root = {
      id: 0,
      height: 0,
      index: 0,
      minIndex: 0,
      maxIndex: 0,
      depth: 0
    };

    this.tree = {
      maxHeight: 0,
      rootNode: root,
      leafNodes: [],
      nLeafNodes: 0
    };
    this.reorderedIndices = [0];
    return;
  }
  // tree array contains array of int left, int right, float distance;
  var tree = linkageAlgorithm(nelements, distmatrix);
  var nodeorder = []; // nNodes;
  var nodecounts = [];// nNodes;
  var order = []; // nelements;
  var nodeID = []; // nNodes;
  for (var i = 0; i < nelements; i++) {
    order[i] = i;
  }
  var leftIds = []; // nNodes
  var rightIds = []; // nNodes
  for (var i = 0; i < nNodes; i++) {
    var min1 = tree[i].left;
    var min2 = tree[i].right;
    /* min1 and min2 are the elements that are to be joined */
    var order1;
    var order2;
    var counts1;
    var counts2;
    var ID1;
    var ID2;
    nodeID[i] = nNodes + (i + 2);
    if (min1 < 0) {
      var index1 = -min1 - 1;
      order1 = nodeorder[index1];
      counts1 = nodecounts[index1];
      ID1 = nodeID[index1];
      tree[i].distance = Math
        .max(tree[i].distance, tree[index1].distance);
    } else {
      order1 = order[min1];
      counts1 = 1;
      ID1 = min1;
    }
    if (min2 < 0) {
      var index2 = -min2 - 1;
      order2 = nodeorder[index2];
      counts2 = nodecounts[index2];
      ID2 = nodeID[index2];
      tree[i].distance = Math
        .max(tree[i].distance, tree[index2].distance);
    } else {
      order2 = order[min2];
      counts2 = 1;
      ID2 = min2;
    }
    leftIds[i] = ID1;
    rightIds[i] = ID2;
    nodecounts[i] = counts1 + counts2;
    nodeorder[i] = (counts1 * order1 + counts2 * order2)
      / (counts1 + counts2);
  }
  var reorderedIndices = phantasus.HCluster.treeSort(nNodes, order, nodeorder,
    nodecounts, tree);
  var idToIndex = {};
  for (var i = 0, length = reorderedIndices.length; i < length; i++) {
    var index = reorderedIndices[i];
    idToIndex[index] = i;
  }
  var nodeIdToNode = {};
  var node;
  for (var i = 0, length = nodeID.length; i < length; i++) {
    var id = nodeID[i];
    var leftId = leftIds[i];
    var lnode = nodeIdToNode[leftId];
    if (lnode === undefined) {
      lnode = {
        id: leftId
      };
      var index = idToIndex[leftId];
      lnode.index = index;
      lnode.minIndex = index;
      lnode.maxIndex = index;
      nodeIdToNode[lnode.id] = lnode;
    }
    var rightId = rightIds[i];
    var rnode = nodeIdToNode[rightId];
    if (rnode === undefined) {
      rnode = {
        id: rightId
      };
      var index = idToIndex[rightId];
      rnode.index = index;
      rnode.minIndex = index;
      rnode.maxIndex = index;
      nodeIdToNode[rnode.id] = rnode;
    }
    node = {
      id: id,
      children: lnode.index < rnode.index ? [lnode, rnode] : [rnode, lnode],
      height: tree[i].distance,
      index: (rnode.index + lnode.index) / 2.0
    };

    node.minIndex = Math.min(rnode.minIndex, lnode.minIndex);
    node.maxIndex = Math.max(rnode.maxIndex, lnode.maxIndex);
    lnode.parent = node;
    rnode.parent = node;
    nodeIdToNode[node.id] = node;
  }
  this.reorderedIndices = reorderedIndices;
  var leafNodes = [];
  for (var i = 0, length = reorderedIndices.length; i < length; i++) {
    var leaf = nodeIdToNode[reorderedIndices[i]];
    leaf.height = 0;
    leafNodes.push(leaf);
  }

  phantasus.DendrogramUtil.setNodeDepths(node);

  this.tree = {
    maxHeight: node.height,
    rootNode: node,
    leafNodes: leafNodes,
    nLeafNodes: leafNodes.length
  };
};
/*
 * Searches the distance matrix to find the pair with the shortest distance
 * between them. The indices of the pair are returned in ip and jp; the distance
 * itself is returned by the function.
 * 
 * @param n The number of elements in the distance matrix.
 * 
 * @param distmatrix. A ragged array containing the distance matrix. The number
 * of columns in each row is one less than the row index.
 * 
 * @return The first and second indices of the pair with the shortest distance.
 */
phantasus.HCluster.findClosestPair = function (n, distmatrix, r) {
  var i, j;
  var temp;
  var distance = distmatrix[1][0];
  var ip = 1;
  var jp = 0;
  for (i = 1; i < n; i++) {
    for (j = 0; j < i; j++) {
      temp = distmatrix[i][j];
      if (temp < distance) {
        distance = temp;
        ip = i;
        jp = j;
      }
    }
  }
  r.distance = distance;
  r.ip = ip;
  r.jp = jp;
};
/**
 * Creates a ragged array with the number of rows equal to the number of rows in
 * the dataset. Each row in the array has n columns where n is the row index.
 *
 * @param dataset
 * @param distanceFunction
 *            The distance function. Use 0 to assume dataset is already a
 *            distance matrix, 1 to assume dataset is already a similarity
 *            matrix.
 * @return the distance matrix
 */
phantasus.HCluster.computeDistanceMatrix = function (dataset, distanceFunction) {
  /* Set up the ragged array */
  var matrix = [];
  var n = dataset.getRowCount();
  for (var i = 1; i < n; i++) {
    matrix[i] = new Float32Array(i);
  }
  // assume dataset is already a distance matrix
  if (distanceFunction === 0) {
    for (var i = 1; i < n; i++) {
      for (var j = 0; j < i; j++) {
        matrix[i][j] = dataset.getValue(i, j);
      }
    }
  } else if (distanceFunction === 1) { // already a similarity matrix
    var max = -Number.MAX_VALUE;
    for (var i = 1; i < n; i++) {
      for (var j = 0; j < i; j++) {
        var value = dataset.getValue(i, j);
        if (!isNaN(value)) {
          max = Math.max(value, max);
        }
      }
    }
    for (var i = 1; i < n; i++) {
      for (var j = 0; j < i; j++) {
        matrix[i][j] = max - dataset.getValue(i, j);
      }
    }
  } else {
    var list1 = new phantasus.DatasetRowView(dataset);
    var list2 = new phantasus.DatasetRowView(dataset);
    /* Calculate the distances and save them in the ragged array */
    for (var i = 1; i < n; i++) {
      list1.setIndex(i);
      for (var j = 0; j < i; j++) {
        matrix[i][j] = distanceFunction(list1, list2.setIndex(j));
      }
    }
  }

  return matrix;
};
phantasus.HCluster.treeSort = function (nNodes, order, nodeorder, nodecounts,
                                       tree) {
  var nElements = nNodes + 1;
  var i;
  var neworder = []; // nElements;
  var clusterids = []; // nElements;
  for (i = 0; i < nElements; i++) {
    clusterids[i] = i;
    neworder[i] = 0;
  }
  for (i = 0; i < nNodes; i++) {
    var i1 = tree[i].left;
    var i2 = tree[i].right;
    var order1 = (i1 < 0) ? nodeorder[-i1 - 1] : order[i1];
    var order2 = (i2 < 0) ? nodeorder[-i2 - 1] : order[i2];
    var count1 = (i1 < 0) ? nodecounts[-i1 - 1] : 1;
    var count2 = (i2 < 0) ? nodecounts[-i2 - 1] : 1;
    /*
     * If order1 and order2 are equal, their order is determined by the
     * order in which they were clustered
     */
    if (i1 < i2) {
      var increase = (order1 < order2) ? count1 : count2;
      var j;
      for (j = 0; j < nElements; j++) {
        var clusterid = clusterids[j];
        if ((clusterid == i1) && (order1 >= order2)) {
          neworder[j] += increase;
        }
        if ((clusterid == i2) && (order1 < order2)) {
          neworder[j] += increase;
        }
        if ((clusterid == i1) || (clusterid == i2)) {
          clusterids[j] = -i - 1;
        }
      }
    } else {
      var increase = (order1 <= order2) ? count1 : count2;
      var j;
      for (j = 0; j < nElements; j++) {
        var clusterid = clusterids[j];
        if ((clusterid == i1) && (order1 > order2)) {
          neworder[j] += increase;
        }
        if ((clusterid == i2) && (order1 <= order2)) {
          neworder[j] += increase;
        }
        if ((clusterid == i1) || (clusterid == i2)) {
          clusterids[j] = -i - 1;
        }
      }
    }
  }
  return phantasus.Util.indexSort(neworder, true);
};

/**
 * Clustering algorithm based on David Arthur and Sergei Vassilvitski k-means++ algorithm.
 * @param <T> type of the points to cluster
 * @see <a href="http://en.wikipedia.org/wiki/K-means%2B%2B">K-means++ (wikipedia)</a>
 * @since 3.2
 */

/**
 * @param dataset The dataset to cluster the rows of
 * @param distance
 * @param k The number of clusters
 * @param maxIterations The maximum number of iterations
 * @constructor
 */
phantasus.KMeansPlusPlusClusterer = function (k, maxIterations, distanceFunction) {

  var distance = function (points1, points2) {
    return distanceFunction(phantasus.VectorUtil.arrayAsVector(points1.getPoint()), phantasus.VectorUtil.arrayAsVector(points2.getPoint()));
  };

  function nextInt(upperBound) {
    return (Math.floor(Math.random() * upperBound)) | 0;
  }

  function nextDouble() {
    return Math.random();
  }

  function PointWrapper(point) {
    this.getPoint = function () {
      return point;
    };
  }

  function CentroidCluster(center) {
    var centroidPoints = [];
    this.addPoint = function (p) {
      centroidPoints.push(p);
    };
    this.getPoints = function () {
      return centroidPoints;
    };
    this.getCenter = function () {
      return center;
    };
  }

  /**
   * Runs the K-means++ clustering algorithm.
   *
   * @param points the points to cluster
   * @return a list of clusters containing the points
   */
  function cluster(points) {
    // number of clusters has to be smaller or equal the number of data points
    if (points.length < k) {
      throw 'Too many clusters';
    }

    // create the initial clusters
    var clusters = chooseInitialCenters(points);

    // create an array containing the latest assignment of a point to a cluster
    // no need to initialize the array, as it will be filled with the first assignment
    var assignments = new Int32Array(points.length);
    assignPointsToClusters(clusters, points, assignments);
    // iterate through updating the centers until we're done
    var max = (maxIterations < 0) ? Number.MAX_VALUE : maxIterations;
    for (var count = 0; count < max; count++) {
      var emptyCluster = false;
      var newClusters = [];
      for (var clusterIndex = 0; clusterIndex < clusters.length; clusterIndex++) {
        var cluster = clusters[clusterIndex];
        var newCenter;
        if (cluster.getPoints().length === 0) {
          newCenter = getPointFromLargestVarianceCluster(clusters);
          emptyCluster = true;
        } else {
          newCenter = centroidOf(cluster.getPoints(), cluster.getCenter().getPoint().size());
        }
        newClusters.push(new CentroidCluster(newCenter));
      }
      var changes = assignPointsToClusters(newClusters, points, assignments);
      clusters = newClusters;

      // if there were no more changes in the point-to-cluster assignment
      // and there are no empty clusters left, return the current clusters
      if (changes === 0 && !emptyCluster) {
        return clusters;
      }
    }
    return clusters;
  }

  /**
   * Adds the given points to the closest {@link Cluster}.
   *
   * @param clusters the {@link Cluster}s to add the points to
   * @param points the points to add to the given {@link Cluster}s
   * @param assignments points assignments to clusters
   * @return the number of points assigned to different clusters as the iteration before
   */
  function assignPointsToClusters(clusters, points, assignments) {
    var assignedDifferently = 0;
    var pointIndex = 0;
    for (var i = 0; i < points.length; i++) {
      var p = points[i];
      var clusterIndex = getNearestCluster(clusters, p);
      if (clusterIndex != assignments[pointIndex]) {
        assignedDifferently++;
      }

      var cluster = clusters[clusterIndex];
      cluster.addPoint(p);
      assignments[pointIndex++] = clusterIndex;
    }

    return assignedDifferently;
  }

  /**
   * Use K-means++ to choose the initial centers.
   *
   * @param points the points to choose the initial centers from
   * @return the initial centers
   */
  function chooseInitialCenters(points) {

    // Convert to list for indexed access. Make it unmodifiable, since removal of items
    // would screw up the logic of this method.
    var pointList = points;
    // The number of points in the list.
    var numPoints = pointList.length;

    // Set the corresponding element in this array to indicate when
    // elements of pointList are no longer available.
    var taken = new Array(numPoints);
    for (var i = 0; i < taken.length; i++) {
      taken[i] = false;
    }

    // The resulting list of initial centers.
    var resultSet = [];

    // Choose one center uniformly at random from among the data points.
    var firstPointIndex = nextInt(numPoints);
    var firstPoint = pointList[firstPointIndex];
    resultSet.push(new CentroidCluster(firstPoint));
    // Must mark it as taken
    taken[firstPointIndex] = true;

    // To keep track of the minimum distance squared of elements of
    // pointList to elements of resultSet.

    var minDistSquared = new Float32Array(numPoints);

    // Initialize the elements.  Since the only point in resultSet is firstPoint,
    // this is very easy.
    for (var i = 0; i < numPoints; i++) {
      if (i !== firstPointIndex) { // That point isn't considered
        var d = distance(firstPoint, pointList[i]);
        minDistSquared[i] = d * d;
      }
    }

    while (resultSet.length < k) {
      // Sum up the squared distances for the points in pointList not
      // already taken.
      var distSqSum = 0.0;

      for (var i = 0; i < numPoints; i++) {
        if (!taken[i]) {
          distSqSum += minDistSquared[i];
        }
      }

      // Add one new data point as a center. Each point x is chosen with
      // probability proportional to D(x)2
      var r = nextDouble() * distSqSum;

      // The index of the next point to be added to the resultSet.
      var nextPointIndex = -1;

      // Sum through the squared min distances again, stopping when
      // sum >= r.
      var sum = 0.0;
      for (var i = 0; i < numPoints; i++) {
        if (!taken[i]) {
          sum += minDistSquared[i];
          if (sum >= r) {
            nextPointIndex = i;
            break;
          }
        }
      }

      // If it's not set to >= 0, the point wasn't found in the previous
      // for loop, probably because distances are extremely small.  Just pick
      // the last available point.
      if (nextPointIndex === -1) {
        for (var i = numPoints - 1; i >= 0; i--) {
          if (!taken[i]) {
            nextPointIndex = i;
            break;
          }
        }
      }

      // We found one.
      if (nextPointIndex >= 0) {

        var p = pointList[nextPointIndex];

        resultSet.push(new CentroidCluster(p));

        // Mark it as taken.
        taken[nextPointIndex] = true;

        if (resultSet.length < k) {
          // Now update elements of minDistSquared.  We only have to compute
          // the distance to the new center to do this.
          for (var j = 0; j < numPoints; j++) {
            // Only have to worry about the points still not taken.
            if (!taken[j]) {
              var d = distance(p, pointList[j]);
              var d2 = d * d;
              if (d2 < minDistSquared[j]) {
                minDistSquared[j] = d2;
              }
            }
          }
        }

      } else {
        // None found --
        // Break from the while loop to prevent
        // an infinite loop.
        break;
      }
    }
    return resultSet;
  }

  /**
   * Get a random point from the {@link Cluster} with the largest distance variance.
   *
   * @param clusters the {@link Cluster}s to search
   * @return a random point from the selected cluster
   * @throws ConvergenceException if clusters are all empty
   */
  function getPointFromLargestVarianceCluster(clusters) {
    var maxVariance = -Number.MAX_VALUE;
    var selected = null;
    for (var clusterIndex = 0; clusterIndex < clusters.length; clusterIndex++) {
      var cluster = clusters[clusterIndex];

      if (cluster.getPoints().length > 0) {

        // compute the distance variance of the current cluster
        var center = cluster.getCenter();
        var points = cluster.getPoints();
        var distances = new Float32Array(points.length);
        for (var i = 0; i < points.length; i++) {
          distances[i] = distance(points[i], center);
        }
        var variance = phantasus.Variance(phantasus.VectorUtil.arrayAsVector(distances));
        // select the cluster with the largest variance
        if (variance > maxVariance) {
          maxVariance = variance;
          selected = cluster;
        }
      }
    }

    // did we find at least one non-empty cluster ?
    if (selected == null) {
      throw 'All clusters are empty';
    }

    // extract a random point from the cluster
    var selectedPoints = selected.getPoints();
    return selectedPoints.splice(nextInt(selectedPoints.length), 1);

  }

  // /**
  //  * Get the point farthest to its cluster center
  //  *
  //  * @param clusters the {@link Cluster}s to search
  //  * @return point farthest to its cluster center
  //  * @throws ConvergenceException if clusters are all empty
  //  */
  // function getFarthestPoint(clusters) {
  //
  //   var maxDistance = Number.NEGATIVE_INFINITY;
  //   var selectedCluster = null;
  //   var selectedPoint = -1;
  //   for (var clusterIndex = 0; clusterIndex < clusters.length; clusterIndex++) {
  //     var c = clusters[clusterIndex];
  //     // get the farthest point
  //     var center = cluster.getCenter();
  //     var points = cluster.getPoints();
  //     for (var i = 0; i < points.length; ++i) {
  //       var d = distance(points[i], center);
  //       if (d > maxDistance) {
  //         maxDistance = d;
  //         selectedCluster = cluster;
  //         selectedPoint = i;
  //       }
  //     }
  //
  //   }
  //
  //   // did we find at least one non-empty cluster ?
  //   if (selectedCluster == null) {
  //     throw 'Empty cluster';
  //   }
  //
  //   return selectedCluster.getPoints().splice(selectedPoint, 1);
  //
  // }

  /**
   * Returns the nearest {@link Cluster} to the given point
   *
   * @param clusters the {@link Cluster}s to search
   * @param point the point to find the nearest {@link Cluster} for
   * @return the index of the nearest {@link Cluster} to the given point
   */
  function getNearestCluster(clusters, point) {
    var minDistance = Number.MAX_VALUE;
    var clusterIndex = 0;
    var minCluster = 0;
    for (var i = 0; i < clusters.length; i++) {
      var c = clusters[i];
      var d = distance(point, c.getCenter());
      if (d < minDistance) {
        minDistance = d;
        minCluster = clusterIndex;
      }
      clusterIndex++;
    }
    return minCluster;
  }

  /**
   * Computes the centroid for a set of points.
   *
   * @param points the set of points
   * @param dimension the point dimension
   * @return the computed centroid for the set of points
   */
  function centroidOf(points, dimension) {
    var centroid = new Float32Array(dimension);
    for (var i = 0; i < centroid.length; i++) {
      var sum = 0;
      var count = 0;
      for (var pointIndex = 0; pointIndex < points.length; pointIndex++) {
        var p = points[pointIndex];
        var point = p.getPoint();
        var val = point.getValue(i);
        if (!isNaN(val)) {
          sum += val;
          count++;
        }
      }
      centroid[i] = (sum / count);
    }
    return new PointWrapper(phantasus.VectorUtil.arrayAsVector(centroid));
  }

  this.execute = function (vectors) {
    var points = [];
    // cluster rows
    var npoints = vectors.length;
    for (var i = 0; i < npoints; i++) {
      var p = new PointWrapper(vectors[i]);
      p.i = i;
      points.push(p);
    }
    return cluster(points);
  };
  this.cluster = cluster;
};

phantasus.PermutationPValues = function (dataset, aIndices, bIndices,
                                        numPermutations, f, continuousList) {
  var numRows = dataset.getRowCount();
  /** unpermuted scores */
  var scores = new Float32Array(numRows);
  /** Whether to smooth p values */
  var smoothPValues = true;
  var permuter;
  var permutationScore;
  if (aIndices != null) {
    var list1 = new phantasus.DatasetRowView(new phantasus.SlicedDatasetView(
      dataset, null, aIndices));
    var list2 = new phantasus.DatasetRowView(new phantasus.SlicedDatasetView(
      dataset, null, bIndices));
    dataset = new phantasus.SlicedDatasetView(dataset, null, aIndices
    .concat(bIndices));
    permuter = new phantasus.UnbalancedPermuter(aIndices.length,
      bIndices.length);
    permutationScore = new phantasus.TwoClassPermutationScore();
    permutationScore.init(dataset, f);
    for (var i = 0; i < numRows; i++) {
      scores[i] = f(list1.setIndex(i), list2.setIndex(i));
    }
  } else { // continuous
    permuter = new phantasus.UnbalancedContinuousPermuter(continuousList.size());
    permutationScore = new phantasus.ContinuousPermutationScore(continuousList);
    permutationScore.init(dataset, f);
    var list = new phantasus.DatasetRowView(dataset);
    for (var i = 0; i < numRows; i++) {
      scores[i] = f(continuousList, list.setIndex(i));
    }
  }

  var rowSpecificPValues = new Float32Array(numRows);

  for (var permutationIndex = 0; permutationIndex < numPermutations; permutationIndex++) {
    permutationScore.setPermutation(permuter.next());
    for (var i = 0; i < numRows; i++) {
      var permutedScore = permutationScore.getScore(i);
      var score = scores[i];
      if (permutedScore >= score) {
        rowSpecificPValues[i]++;
      }
    }
  }
  var N = numPermutations;
  var kArray = new Uint32Array(numRows);
  for (var i = 0; i < numRows; i++) {
    var k = rowSpecificPValues[i];
    kArray[i] = k;
    var p;
    if (smoothPValues) {
      p = (k + 1) / (N + 2);
    } else {
      p = k / N;

    }
    // 2-sided p-value
    var oneMinusP = 1 - p;
    if (oneMinusP < p) {
      p = oneMinusP;
    }
    p *= 2;
    if (p === 0) {
      // ensure not degenerate case where profile is
      // completely
      // flat
      // TODO handle cases where profile is flat (but not
      // completely)

      var val = dataset.getValue(i, 0);
      var flat = true;
      for (var j = 1, cols = dataset.getColumnCount(); j < cols && flat; j++) {
        if (dataset.getValue(i, j) != val) {
          flat = false;
        }
      }
      if (flat) {
        p = 1;
      }
    }
    rowSpecificPValues[i] = p;

  }
  this.rowSpecificPValues = rowSpecificPValues;
  this.k = kArray;
  this.fdr = phantasus.FDR_BH(rowSpecificPValues);
  this.scores = scores;
};
phantasus.PermutationPValues.prototype = {
  getBonferroni: function (index) {
    return Math.min(this.rowSpecificPValues[index] * this.numRows, 1);
  }
};

phantasus.UnbalancedContinuousPermuter = function (size) {
  var indices = new Uint32Array(size);
  for (var i = 0; i < indices.length; i++) {
    indices[i] = i;
  }
  var n = indices.length;
  // Returns a random integer between min (included) and max (included)
  // Using Math.round() will give you a non-uniform distribution!
  function getRandomIntInclusive(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }

  this.next = function () {
    // shuffle indices array
    for (var i = n - 1; i >= 1; i--) {
      var j = getRandomIntInclusive(0, i); // random integer such that
      // 0 ≤ j ≤ i
      // exchange a[j] and a[i]
      var tmp = indices[j];
      indices[j] = indices[i];
      indices[i] = tmp;
    }

    return indices;
  };
};

phantasus.UnbalancedPermuter = function (numClassZero, numClassOne) {
  var assignments = new Uint32Array(numClassZero + numClassOne);
  var indices = new Uint32Array(numClassZero + numClassOne);
  for (var i = 0; i < indices.length; i++) {
    indices[i] = i;
  }
  var n = indices.length;
  // Returns a random integer between min (included) and max (included)
  // Using Math.round() will give you a non-uniform distribution!
  function getRandomIntInclusive(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }

  this.next = function () {
    // shuffle indices array
    for (var i = n - 1; i >= 1; i--) {
      var j = getRandomIntInclusive(0, i); // random integer such that
      // 0 ≤ j ≤ i
      // exchange a[j] and a[i]
      var tmp = indices[j];
      indices[j] = indices[i];
      indices[i] = tmp;
    }

    // pick 1st numClassOne indices to be class one
    for (var i = 0; i < n; i++) {
      assignments[i] = 0;
    }
    for (var i = 0; i < numClassOne; i++) {
      assignments[indices[i]] = 1;
    }

    return assignments;
  };
};

phantasus.TwoClassPermutationScore = function () {
  this.classZeroView = null;
  this.classOneView = null;

};
phantasus.TwoClassPermutationScore.prototype = {
  getScore: function (index) {
    this.classZeroView.setIndex(index);
    this.classOneView.setIndex(index);
    return this.f(this.classZeroView, this.classOneView);
  },
  init: function (dataset, f) {
    this.dataset = dataset;
    this.classZeroView = new phantasus.DatasetRowView(dataset);
    this.classOneView = new phantasus.DatasetRowView(dataset);
    this.f = f;
  },
  setPermutation: function (permutedAssignments) {
    var zeroIndices = [];
    var oneIndices = [];
    for (var i = 0, length = permutedAssignments.length; i < length; i++) {
      if (permutedAssignments[i] === 0) {
        zeroIndices.push(i);
      } else {
        oneIndices.push(i);
      }
    }

    this.classZeroView.setDataset(new phantasus.SlicedDatasetView(
      this.dataset, null, zeroIndices));
    this.classOneView.setDataset(new phantasus.SlicedDatasetView(
      this.dataset, null, oneIndices));

  }

};

phantasus.ContinuousPermutationScore = function (referenceList) {
  this.referenceList = referenceList;
};
phantasus.ContinuousPermutationScore.prototype = {
  getScore: function (index) {
    this.datasetRowView.setIndex(index);
    return this.f(this.referenceListView, this.datasetRowView);
  },
  init: function (dataset, f) {
    this.dataset = dataset;
    this.referenceListView = null;
    this.datasetRowView = new phantasus.DatasetRowView(dataset);
    this.f = f;
  },
  setPermutation: function (indices) {
    this.referenceListView = new phantasus.SlicedVector(this.referenceList, indices);
  }

};

phantasus.Ranking = function (values) {
  var ranks = [];
  for (var i = 0, length = values.length; i < length; i++) {
    ranks.push({
      value: values[i],
      position: i
    });
  }
  if (ranks.length === 0) {
    return [];
  }
  ranks.sort(function (a, b) {
    return (a.value < b.value ? -1 : (a.value === b.value ? 0 : 1));
  });

  var out = [];
  var pos = 1; // position in sorted array
  out[ranks[0].position] = pos;
  var tiesTrace = [];
  tiesTrace.push(ranks[0].position);
  for (var i = 1; i < ranks.length; i++) {
    if (ranks[i].value > ranks[i - 1].value) {
      // tie sequence has ended (or had length 1)
      pos = i + 1;
      if (tiesTrace.length > 1) { // if seq is nontrivial, resolve
        phantasus.Ranking.fillAverage(out, tiesTrace);
      }
      tiesTrace = [];
      tiesTrace.push(ranks[i].position);
    } else {
      // tie sequence continues
      tiesTrace.push(ranks[i].position);
    }
    out[ranks[i].position] = pos;
  }
  if (tiesTrace.length > 1) { // handle tie sequence at end
    phantasus.Ranking.fillAverage(out, tiesTrace);
  }
  return out;
};
phantasus.Ranking.fill = function (data, tiesTrace, value) {
  for (var i = 0, length = tiesTrace.length; i < length; i++) {
    data[tiesTrace[i]] = value;
  }
};
phantasus.Ranking.fillAverage = function (ranks, tiesTrace) {
  var c = ranks[tiesTrace[0]];
  // length of sequence of tied ranks
  var length = tiesTrace.length;
  phantasus.Ranking.fill(ranks, tiesTrace, (2 * c + length - 1) / 2);
};

phantasus.SingleLinkage = function (nelements, distmatrix) {
  var i, j, k;
  var nnodes = nelements - 1;
  var temp = []; // var[nnodes];
  var index = []; // new var[nelements];
  var vector = []; // var[nnodes];
  var result = []; // new Node[nelements];
  for (i = 0; i < nelements; i++) {
    result[i] = {
      left: 0,
      right: 0,
      distance: 0
    };
  }
  for (i = 0; i < nnodes; i++) {
    vector[i] = i;
  }
  for (i = 0; i < nelements; i++) {
    result[i].distance = Number.MAX_VALUE;
    for (j = 0; j < i; j++) {
      temp[j] = distmatrix[i][j];
    }
    for (j = 0; j < i; j++) {
      k = vector[j];
      if (result[j].distance >= temp[j]) {
        if (result[j].distance < temp[k]) {
          temp[k] = result[j].distance;
        }
        result[j].distance = temp[j];
        vector[j] = i;
      } else if (temp[j] < temp[k]) {
        temp[k] = temp[j];
      }
    }
    for (j = 0; j < i; j++) {
      if (result[j].distance >= result[vector[j]].distance) {
        vector[j] = i;
      }
    }
  }
  for (i = 0; i < nnodes; i++) {
    result[i].left = i;
  }
  result.sort(function (node1, node2) {
    var term1 = node1.distance;
    var term2 = node2.distance;
    if (term1 < term2)
      return -1;
    if (term1 > term2)
      return +1;
    return 0;
  });
  for (i = 0; i < nelements; i++) {
    index[i] = i;
  }
  for (i = 0; i < nnodes; i++) {
    j = result[i].left;
    k = vector[j];
    result[i].left = index[j];
    result[i].right = index[k];
    index[k] = -i - 1;
  }
  var result2 = []; // new Node[nelements - 1];
  for (i = 0; i < nelements - 1; i++) {
    result2[i] = result[i];
  }
  return result2;
};

})(typeof window !== 'undefined' ? window : this);
