define([
'jquery',
'underscore',
'view',
'slickgrid',
'chosen',
'abcviewcontroller',
'scale-editor',
'util'
], function($, _, DecompositionView, SlickGrid, Chosen, abc, ScaleEditor,
util) {
EmperorViewControllerABC = abc.EmperorViewControllerABC;
/**
*
* @class EmperorViewController
*
* Base class for view controllers that use a dictionary of decomposition
* views, but that are not controlled by a metadata category, for those
* cases, see `EmperorAttributeABC`.
*
* @param {UIState} uiState The shared state
* @param {Node} container Container node to create the controller in.
* @param {String} title title of the tab.
* @param {String} description helper description.
* @param {Object} decompViewDict This is object is keyed by unique
* identifiers and the values are DecompositionView objects referring to a
* set of objects presented on screen. This dictionary will usually be shared
* by all the tabs in the application. This argument is passed by reference.
*
* @return {EmperorViewController} Returns an instance of the
* EmperorViewController class.
* @constructs EmperorViewController
* @extends EmperorViewControllerABC
*
*/
function EmperorViewController(uiState, container, title, description,
decompViewDict) {
EmperorViewControllerABC.call(this, uiState, container, title, description);
if (decompViewDict === undefined) {
throw Error('The decomposition view dictionary cannot be undefined');
}
for (var dv in decompViewDict) {
if (!dv instanceof DecompositionView) {
throw Error('The decomposition view dictionary ' +
'can only have decomposition views');
}
}
if (_.size(decompViewDict) <= 0) {
throw Error('The decomposition view dictionary cannot be empty');
}
/**
* @type {Object}
* This is object is keyed by unique identifiers and the values are
* DecompositionView objects referring to a set of objects presented on
* screen. This dictionary will usually be shared by all the tabs in the
* application. This argument is passed by reference.
*/
this.decompViewDict = decompViewDict;
/**
* @type {Function}
* Callback to execute when all the elements in the UI for this controller
* have been loaded. Note, that this functionality needs to be implemented
* by subclasses, as EmperorViewController does not have any UI components.
*/
this.ready = null;
return this;
}
EmperorViewController.prototype = Object.create(
EmperorViewControllerABC.prototype);
EmperorViewController.prototype.constructor = EmperorViewControllerABC;
/**
*
* Retrieve a view from the controller.
*
* This class does not operate on single decomposition views, hence this
* method retrieves the first available view.
*
*/
EmperorViewController.prototype.getView = function() {
// return the first decomposition view available in the dictionary
return this.decompViewDict[Object.keys(this.decompViewDict)[0]];
};
/**
* Check if a metadata field is present
*
* @param {String} m Metadata column to check if is present.
*
* @return {Bool} Whether or not the metadata field is present.
*
*/
EmperorViewController.prototype.hasMetadataField = function(m) {
// loop through the metadata headers in the decompositon views
// FIXME: There's no good way to specify the current decomposition name
// this needs to be added to the interface.
var res = _.find(this.decompViewDict, function(view) {
return view.decomp.md_headers.indexOf(m) !== -1;
});
return res !== undefined;
};
/**
*
* @class EmperorAttributeABC
*
* Initializes an abstract tab for attributes i.e. shape, color, size, etc.
* This has to be contained in a DOM object and will use the full size of
* that container.
*
* @param {UIState} uiState the shared state
* @param {Node} container Container node to create the controller in.
* @param {String} title title of the tab.
* @param {String} description helper description.
* @param {Object} decompViewDict This is object is keyed by unique
* identifiers and the values are DecompositionView objects referring to a
* set of objects presented on screen. This dictionary will usually be shared
* by all the tabs in the application. This argument is passed by reference.
* @param {Object} options This is a dictionary of options used to build
* the view controller. Used to set attributes of the slick grid and the
* metadata category drop down. At the moment the constructor only expects
* the following attributes:
* - categorySelectionCallback: a function object that's called when a new
* metadata category is selected in the dropdown living in the header.
* See [change]{@link https://api.jquery.com/change/}.
* - valueUpdatedCallback: a function object that's called when a metadata
* visualization attribute is modified (i.e. a change of color).
* See [onCellChange]{@link
* https://github.com/mleibman/SlickGrid/wiki/Grid-Events}.
* - slickGridColumn: a dictionary specifying options to be passed into the
* slickGrid. For instance, the ColorFormatter and the ColorEditor would be
* passed here. For more information, refer to the Slick Grid
* documentation.
*
* @return {EmperorAttributeABC} Returns an instance of the
* EmperorAttributeABC class.
* @constructs EmperorAttributeABC
* @extends EmperorViewController
*
*/
function EmperorAttributeABC(uiState, container, title, description,
decompViewDict, options) {
EmperorViewController.call(this, uiState, container, title, description,
decompViewDict);
/**
* @type {Object}
* Dictionary-like object where keys are metadata categories and values are
* lists of metadata columns. This object reflects the data presented in
* the metadata menu.
* @private
*/
this._metadata = {};
/**
* @type {Node}
* jQuery element for the div containing the slickgrid of sample information
*/
this.$gridDiv = $('<div name="emperor-grid-div"></div>');
this.$gridDiv.css('margin', '0 auto');
this.$gridDiv.css('width', '100%');
this.$gridDiv.css('height', '100%');
this.$gridDiv.attr('title', 'Change the ' + title.toLowerCase() + ' with' +
' the left column controls.');
this.$body.append(this.$gridDiv);
var dm = this.getView().decomp;
var scope = this;
// http://stackoverflow.com/a/6602002
this.$select = $('<select>');
this.$header.append(this.$select);
this.$searchBar = $("<input type='search' " +
"placeholder='Search for a value ...'>"
).css({
'width': '100%'
});
this.$header.append(this.$searchBar);
// there's a few attributes we can only set on "ready" so list them up here
$(function() {
scope.$searchBar.tooltip({
content: 'No results found!',
disabled: true,
// place the element with a slight offset at the bottom of the input
// so that it doesn't overlap with the "continuous values" elements
position: {my: 'center top+40', at: 'center bottom',
of: scope.$searchBar},
// prevent the tooltip from disappearing when there's no matches
close: function(event, ui) {
if (scope.bodyGrid.getDataLength() === 0 &&
scope.$searchBar.val() !== '') {
scope.$searchBar.tooltip('open');
}
}
});
var placeholder = 'Select a ' + scope.title + ' Category';
// setup the slick grid
scope._buildGrid(options);
scope.refreshMetadata();
// once this element is ready, it is safe to execute the "ready" callback
// if a subclass needs to wait on other elements, this attribute should
// be changed to null so this callback is effectively cancelled, for an
// example see the constructor of ColorViewController
scope.$select.on('chosen:ready', function() {
if (scope.ready !== null) {
scope.ready();
}
});
// setup chosen
scope.$select.chosen({width: '100%', search_contains: true,
include_group_label_in_selected: true,
placeholder_text_single: placeholder});
// only subclasses will provide this callback
if (options.categorySelectionCallback !== undefined) {
// Disable interface controls (except the metadata selector) to
// prevent errors while no metadata category is selected. Once the
// user selects a metadata category, the controls will be enabled
// (see setSlickGridDataset).
scope.setEnabled(false);
scope.$select.val('');
scope.$select.prop('disabled', false).trigger('chosen:updated');
scope.$select.chosen().change(options.categorySelectionCallback);
}
// general events
scope._setupEvents();
});
return this;
}
EmperorAttributeABC.prototype = Object.create(
EmperorViewController.prototype);
EmperorAttributeABC.prototype.constructor = EmperorViewController;
/**
*
* Get the name of the decomposition selected in the metadata menu.
*
*/
EmperorAttributeABC.prototype.decompositionName = function(cat) {
return this.$select.find(':selected').parent().attr('label');
};
/**
*
* Get the view that's currently selected by the metadata menu.
*
*/
EmperorAttributeABC.prototype.getView = function() {
var view;
try {
view = this.decompViewDict[this.decompositionName()];
}
catch (TypeError) {
view = EmperorViewController.prototype.getView.call(this);
}
return view;
};
/**
*
* Private method to reset the attributes of the controller.
*
* Subclasses should implement this method as a way to reset the visual
* attributes of a given plot.
* @private
*
*/
EmperorAttributeABC.prototype._resetAttribute = function() {
};
/**
* Changes the selected value in the metadata menu.
*
* @param {String} m Metadata column name to control. When the category is
* ``null``, the metadata selector is set to an empty value, the body grid
* is emptied, and all the markers are reset to a default state (depends on
* the subclass).
*
* @throws {Error} Argument `m` must be a metadata category in one of the
* decomposition views.
*/
EmperorAttributeABC.prototype.setMetadataField = function(m) {
if (m === null) {
this._resetAttribute();
this.$select.val('');
this.setSlickGridDataset([]);
this.setEnabled(false);
this.$select.prop('disabled', false).trigger('chosen:updated');
return;
}
if (!this.hasMetadataField(m)) {
throw Error('Cannot set "' + m + '" as the metadata field, this column' +
' is not available in the decomposition views');
}
this.$select.val(m);
this.$select.trigger('chosen:updated');
this.$select.change();
};
/**
*
* Get the name of the selected category in the metadata menu.
*
*/
EmperorAttributeABC.prototype.getMetadataField = function() {
return this.$select.val();
};
/**
* Retrieves the underlying data in the slick grid
* @return {Array} Returns an array of objects
* displayed by the body grid.
*/
EmperorAttributeABC.prototype.getSlickGridDataset = function() {
return this.bodyGrid.getData().getItems();
};
/**
* Changes the underlying data in the slick grid
*
* @param {Array} data data.
*/
EmperorAttributeABC.prototype.setSlickGridDataset = function(data) {
// Accounts for cases where controllers have not been set to a metadata
// category. In these cases all controllers (except for the metadata
// selector) are disabled to prevent interface errors.
if (this.getSlickGridDataset().length === 0 && this.enabled === false) {
this.setEnabled(true);
}
// Re-render the grid on the DOM
this.bodyGrid.getData().beginUpdate();
this.bodyGrid.getData().setItems(data);
this.bodyGrid.getData().endUpdate();
this.bodyGrid.invalidate();
this.bodyGrid.render();
};
/**
* Method in charge of initializing the SlickGrid object
*
* @param {Object} [options] additional options to initialize the slick grid
* of this object.
* @private
*
*/
EmperorAttributeABC.prototype._buildGrid = function(options) {
var columns = [{id: 'field1', name: '', field: 'category'}], scope = this;
// autoEdit enables one-click editor trigger on the entire grid, instead
// of requiring users to click twice on a widget.
var gridOptions = {editable: true, enableAddRow: false,
enableCellNavigation: true, forceFitColumns: true,
enableColumnReorder: false, autoEdit: true};
// If there's a custom slickgrid column then add it to the object
if (options.slickGridColumn !== undefined) {
columns.unshift(options.slickGridColumn);
}
var dataView = new Slick.Data.DataView(), searchString = '';
/**
* @type {Slick.Grid}
* Container that lists the metadata categories described under the
* metadata column and the attribute that can be modified.
*/
this.bodyGrid = new Slick.Grid(this.$gridDiv, dataView, columns,
gridOptions);
this.$searchBar.on('input', function(e) {
dataView.refresh();
// show a message when no results are found
if (scope.bodyGrid.getDataLength() === 0 &&
scope.$searchBar.val() !== '') {
scope.$searchBar.tooltip('option', 'disabled', false);
scope.$searchBar.tooltip('open');
}
else {
scope.$searchBar.tooltip('option', 'disabled', true);
scope.$searchBar.tooltip('close');
}
});
function substringFilter(item, args) {
var val = scope.$searchBar.val();
if (!searchString && val &&
item.category.toLowerCase().indexOf(val.toLowerCase()) === -1) {
return false;
}
return true;
}
dataView.onRowCountChanged.subscribe(function(e, args) {
scope.bodyGrid.updateRowCount();
scope.bodyGrid.render();
});
dataView.onRowsChanged.subscribe(function(e, args) {
scope.bodyGrid.invalidateRows(args.rows);
scope.bodyGrid.render();
});
dataView.setFilter(substringFilter);
// hide the header row of the grid
// http://stackoverflow.com/a/29827664/379593
$(this.$body).find('.slick-header').css('display', 'none');
// subscribe to events when a cell is changed
this.bodyGrid.onCellChange.subscribe(options.valueUpdatedCallback);
};
EmperorAttributeABC.prototype._setupEvents = function() {
var scope = this;
// dispatch an event when the category changes
this.$select.on('change', function() {
scope.dispatchEvent({type: 'category-changed',
message: {category: scope.getMetadataField(),
controller: scope}
});
});
// dispatch an event when a value changes and send the plottable objects
this.bodyGrid.onCellChange.subscribe(function(e, args) {
scope.dispatchEvent({type: 'value-changed',
message: {category: scope.getMetadataField(),
attribute: args.item.value,
group: args.item.plottables,
controller: scope}
});
});
// dispatch an event when a category is double-clicked
this.bodyGrid.onDblClick.subscribe(function(e, args) {
var item = scope.bodyGrid.getDataItem(args.row);
scope.dispatchEvent({type: 'value-double-clicked',
message: {category: scope.getMetadataField(),
value: item.category,
attribute: item.value,
group: item.plottables,
controller: scope}
});
});
};
/**
* Resizes the container and the individual elements.
*
* Note, the consumer of this class, likely the main controller should call
* the resize function any time a resizing event happens.
*
* @param {Float} width the container width.
* @param {Float} height the container height.
*/
EmperorAttributeABC.prototype.resize = function(width, height) {
// call super, most of the header and body resizing logic is done there
EmperorViewController.prototype.resize.call(this, width, height);
// the whole code is asynchronous, so there may be situations where
// bodyGrid doesn't exist yet, so check before trying to modify the object
if (this.bodyGrid !== undefined) {
// make the columns fit the available space whenever the window resizes
// http://stackoverflow.com/a/29835739
this.bodyGrid.setColumns(this.bodyGrid.getColumns());
// Resize the slickgrid canvas for the new body size.
this.bodyGrid.resizeCanvas();
}
};
/**
* Converts the current instance into a JSON object.
*
* @return {Object} base object ready for JSON conversion.
*/
EmperorAttributeABC.prototype.toJSON = function() {
var json = {};
json.category = this.getMetadataField();
// Convert SlickGrid list of objects to single object
var gridData = this.getSlickGridDataset();
var jsonData = {};
for (var i = 0; i < gridData.length; i++) {
jsonData[gridData[i].category] = gridData[i].value;
}
json.data = jsonData;
return json;
};
/**
* Decodes JSON string and modifies its own instance variables accordingly.
*
* @param {Object} json Parsed JSON string representation of self.
*
*/
EmperorAttributeABC.prototype.fromJSON = function(json) {
this.setMetadataField(json.category);
// if the category is null, then we just reset the controller
if (json.category !== null) {
// fetch and set the SlickGrid-formatted data
var data = this.getView().setCategory(
json.data, this.setPlottableAttributes, json.category);
this.setSlickGridDataset(data);
// set all to needsUpdate
this.getView().needsUpdate = true;
}
};
/**
*
* Update the metadata selection menu.
*
* Performs some additional logic to avoid duplicating decomposition names.
*
* Note that decompositions won't be updated if they have the same name and
* same metadata headers, if the only things changing are coordinates, or
* metadata values, the changes should be performed directly on the objects
* themselves.
*
*/
EmperorAttributeABC.prototype.refreshMetadata = function() {
var scope = this, group, hdrs;
_.each(this.decompViewDict, function(view, name) {
// sort alphabetically the metadata headers (
hdrs = _.sortBy(view.decomp.md_headers, function(x) {
return x.toLowerCase();
});
// Before we update the metadata view, we rectify that we don't have that
// information already. The order in this conditional matters as we hope
// to short-circuit if the name is not already present. If that's not
// the case, we also check to ensure the lists are equivalent.
if (_.contains(_.keys(scope._metadata), name) &&
_.intersection(scope._metadata[name], hdrs).length == hdrs.length &&
scope._metadata[name].length == hdrs.length) {
return;
}
// create the new category
scope._metadata[name] = [];
group = $('<optgroup>').attr('label', name);
scope.$select.append(group);
_.each(hdrs, function(header) {
group.append($('<option>').attr('value', header).text(header));
scope._metadata[name].push(header);
});
});
this.$select.trigger('chosen:updated');
};
/**
* Sets whether or not the tab can be modified or accessed.
*
* @param {Boolean} trulse option to enable tab.
*/
EmperorAttributeABC.prototype.setEnabled = function(trulse) {
EmperorViewController.prototype.setEnabled.call(this, trulse);
this.$select.prop('disabled', !trulse).trigger('chosen:updated');
this.bodyGrid.setOptions({editable: trulse});
this.$searchBar.prop('disabled', !trulse);
this.$searchBar.prop('hidden', !trulse);
};
/**
* @class ScalarViewControllerABC
*
* Alters the scale of points displayed on the screen.
*
* @param {UIState} uiState The shared state
* @param {Node} container Container node to create the controller in.
* @param {String} title The name/title of the tab.
* @param {String} helpmenu description helper description.
* @param {Float} min Minimum value for the attribute.
* @param {Float} max Maximum value for the attribute.
* @param {Float} step Size of the step for an attribute slider.
* @param {Object} decompViewDict This object is keyed by unique identifiers
* and the values are DecompositionView objects referring to a set of objects
* presented on screen. This dictionary will usually be shared by all the
* tabs in the application. This argument is passed by reference.
*
* @return {ScalarViewControllerABC}
* @constructs ScalarViewControllerABC
* @extends EmperorAttributeABC
*
**/
function ScalarViewControllerABC(uiState, container, title, helpmenu, min,
max, step, decompViewDict) {
// Create checkbox for scaling by values
/**
* jQuery node for checkbox controlling whether to scale by values or not
* @type {Node}
*/
this.$scaledValue = $('<input type="checkbox">');
/**
* jQuery node for label of $scaledValues
* @type {Node}
*/
this.$scaledLabel = $('<label>Change ' + title.toLowerCase() + ' by ' +
'values</label>');
this.$scaledLabel.attr('title', 'Samples with lower values will have ' +
'a decreased ' + title.toLowerCase());
//Create global scale bar
/**
* jQuery node for global scale bar container div
* @type {Node}
*/
this.$globalDiv = $('<div style="width:100%;padding:5px;">');
this.$globalDiv.html('<p>Global Scaling</p>');
var $sliderDiv = $('<div style="width:80%;display:inline-block;">');
var $viewval = $('<input type="text" value="1.0" readonly ' +
'style="border:0;width:25px;' +
'background-color:rgb(238, 238, 238)">');
/**
* jQuery node for global scale bar
* @type {Node}
*/
this.$sliderGlobal = $sliderDiv.slider({
range: 'max',
min: min,
max: max,
value: 1.0,
step: step,
slide: function(event, ui) {
$viewval.val(ui.value);
},
stop: function(event, ui) {
// Update the slickgrid values with the new scalar
var data = scope.getSlickGridDataset();
_.each(data, function(element) {
element.value = ui.value;
});
scope.setSlickGridDataset(data);
scope.setAllPlottableAttributes(ui.value);
}
});
this.$globalDiv.append($viewval);
this.$globalDiv.append($sliderDiv);
// Constant for width in slick-grid
var SLICK_WIDTH = 50, scope = this;
// Build the options dictionary
var options = {
'valueUpdatedCallback': function(e, args) {
var scalar = +args.item.value;
var group = args.item.plottables;
var element = scope.getView();
scope.setPlottableAttributes(element, scalar, group);
},
'categorySelectionCallback': function(evt, params) {
var category = scope.$select.val();
var decompViewDict = scope.getView();
var attributes;
// getting all unique values per categories
var uniqueVals = decompViewDict.decomp.getUniqueValuesByCategory(
category);
// getting a scalar value for each point
var scaled = scope.$scaledValue.is(':checked');
try {
attributes = scope.getScale(uniqueVals, scaled);
}
catch (err) {
scope.$scaledValue.attr('checked', false);
return;
}
if (scaled) {
scope.$globalDiv.hide();
}
else {
scope.$globalDiv.show();
}
scope.resize();
// fetch the slickgrid-formatted data
var data = decompViewDict.setCategory(attributes,
scope.setPlottableAttributes,
category);
scope.setSlickGridDataset(data);
scope.$sliderGlobal.slider('value', 1);
$viewval.val(1);
},
'slickGridColumn': {id: 'title', name: title, field: 'value',
sortable: false, maxWidth: SLICK_WIDTH,
minWidth: SLICK_WIDTH,
editor: ScaleEditor.ScaleEditor,
formatter: ScaleEditor.ScaleFormatter},
'editorOptions': {'min': min, 'max': max, 'step': step}
};
EmperorAttributeABC.call(this, uiState, container, title, helpmenu,
decompViewDict, options);
this.$header.append(this.$scaledValue);
this.$header.append(this.$scaledLabel);
this.$body.prepend(this.$globalDiv);
scope.$scaledValue.on('change', options.categorySelectionCallback);
return this;
}
ScalarViewControllerABC.prototype = Object.create(
EmperorAttributeABC.prototype);
ScalarViewControllerABC.prototype.constructor = EmperorAttributeABC;
/**
* Converts the current instance into a JSON string.
*
* @return {Object} JSON ready representation of self.
*/
ScalarViewControllerABC.prototype.toJSON = function() {
var json = EmperorAttributeABC.prototype.toJSON.call(this);
json.globalScale = this.$globalDiv.children('input').val();
json.scaleVal = this.$scaledValue.is(':checked');
return json;
};
/**
* Decodes JSON string and modifies its own instance variables accordingly.
*
* @param {Object} Parsed JSON string representation of self.
*/
ScalarViewControllerABC.prototype.fromJSON = function(json) {
// Can't call super because select needs to be set first Order here is
// important. We want to set all the extra controller settings before we
// load from json, as they can override the JSON when set
this.setMetadataField(json.category);
// if the category is null, then there's nothing to set about the state
// of the controller
if (json.category === null) {
return;
}
this.$select.val(json.category);
this.$select.trigger('chosen:updated');
this.$sliderGlobal.slider('value', json.globalScale);
this.$scaledValue.prop('checked', json.scaleVal);
this.$scaledValue.trigger('change');
// fetch and set the SlickGrid-formatted data
var data = this.getView().setCategory(json.data,
this.setPlottableAttributes,
json.category);
this.setSlickGridDataset(data);
// set all to needsUpdate
this.getView().needsUpdate = true;
};
/**
* Resizes the container and the individual elements.
*
* Note, the consumer of this class, likely the main controller should call
* the resize function any time a resizing event happens.
*
* @param {float} width the container width.
* @param {float} height the container height.
*/
ScalarViewControllerABC.prototype.resize = function(width, height) {
this.$body.height(this.$canvas.height() - this.$header.height());
this.$body.width(this.$canvas.width());
//scale gridDiv based on whether global scaling available or not
if (this.$scaledValue.is(':checked')) {
this.$gridDiv.css('height', '100%');
}
else {
this.$gridDiv.css(
'height', this.$body.height() - this.$globalDiv.height() - 10);
}
// call super, most of the header and body resizing logic is done there
EmperorAttributeABC.prototype.resize.call(this, width, height);
};
/**
* Sets whether or not elements in the tab can be modified.
*
* @param {Boolean} trulse option to enable elements.
*/
ScalarViewControllerABC.prototype.setEnabled = function(trulse) {
EmperorAttributeABC.prototype.setEnabled.call(this, trulse);
var color;
this.$scaledValue.prop('disabled', !trulse);
this.$sliderGlobal.slider('option', 'disabled', !trulse);
if (trulse) {
color = '#70caff';
}
else {
color = '';
}
this.$sliderGlobal.css('background', color);
};
/**
*
* Private method to reset the scale of all the objects to one.
*
* @extends EmperorAttributeABC
* @private
*
*/
ScalarViewControllerABC.prototype._resetAttribute = function() {
EmperorAttributeABC.prototype._resetAttribute.call(this);
var scope = this;
this.$scaledValue.prop('checked', false);
_.each(this.decompViewDict, function(view) {
scope.setPlottableAttributes(view, 1, view.decomp.plottable);
view.needsUpdate = true;
});
};
/**
*
* Helper function to set the scale of plottable.
*
* Note, needs to be overriden by the subclass.
*
*/
ScalarViewControllerABC.prototype.setPlottableAttributes = function() {
};
/**
*
* Method to do global updates to only the current view
*
* Note, needs to be overriden by the subclass.
*
*/
ScalarViewControllerABC.prototype.setAllPlottableAttributes = function() {
};
/**
*
* Scaling function to use when the attribute is based on a metadata
* category (used in getScale).
*
* @param {float} val The metadata value for the current sample.
* @param {float} min The minimum metadata value in the dataset.
* @param {float} range The span of the metadata values.
*
* @return {float} Attribute value
*
*/
ScalarViewControllerABC.prototype.scaleValue = function(val, min, range) {
return 1;
};
/**
* Helper function to get the scale for each metadata value
*
* @param {String[]} values The values to get scale for
* @param {Boolean} scaled Whether or not to scale by values or just reset to
* standard scale (1.0)
*
* @throws {Error} No or one numeric value in category and trying to scale by
* value
*/
ScalarViewControllerABC.prototype.getScale = function(values, scaled) {
var scale = {}, numbers, val, scope = this;
if (!scaled) {
_.each(values, function(element) {
scale[element] = 1.0;
});
}
else {
//See if we have numeric values, fail if no
var split = util.splitNumericValues(values);
if (split.numeric.length < 2) {
alert('Not enough numeric values in category, can not scale by value!');
throw new Error('no numeric values');
}
// Alert if we have non-numerics and scale them to 0
if (split.nonNumeric.length > 0) {
_.each(split.nonNumeric, function(element) {
scale[element] = 0.0;
});
alert('Non-numeric values detected. These will be hidden!');
}
// convert objects to numbers so we can map them to a color, we keep a
// copy of the untransformed object so we can search the metadata
numbers = _.map(split.numeric, parseFloat);
//scale remaining values between 1 and 5 scale
var min = _.min(numbers);
var max = _.max(numbers);
var range = max - min;
_.each(split.numeric, function(element) {
// note these elements are not numbers
val = parseFloat(element);
// Scale the values, then round to 4 decimal places.
scale[element] = scope.scaleValue(val, min, range);
});
}
return scale;
};
return {'EmperorViewControllerABC': EmperorViewControllerABC,
'EmperorViewController': EmperorViewController,
'EmperorAttributeABC': EmperorAttributeABC,
'ScalarViewControllerABC': ScalarViewControllerABC};
});