1 define([ 2 'jquery', 3 'underscore', 4 'view', 5 'slickgrid', 6 'chosen', 7 'abcviewcontroller', 8 'scale-editor', 9 'util' 10 ], function($, _, DecompositionView, SlickGrid, Chosen, abc, ScaleEditor, 11 util) { 12 EmperorViewControllerABC = abc.EmperorViewControllerABC; 13 14 /** 15 * 16 * @class EmperorViewController 17 * 18 * Base class for view controllers that use a dictionary of decomposition 19 * views, but that are not controlled by a metadata category, for those 20 * cases, see `EmperorAttributeABC`. 21 * 22 * @param {UIState} uiState The shared state 23 * @param {Node} container Container node to create the controller in. 24 * @param {String} title title of the tab. 25 * @param {String} description helper description. 26 * @param {Object} decompViewDict This is object is keyed by unique 27 * identifiers and the values are DecompositionView objects referring to a 28 * set of objects presented on screen. This dictionary will usually be shared 29 * by all the tabs in the application. This argument is passed by reference. 30 * 31 * @return {EmperorViewController} Returns an instance of the 32 * EmperorViewController class. 33 * @constructs EmperorViewController 34 * @extends EmperorViewControllerABC 35 * 36 */ 37 function EmperorViewController(uiState, container, title, description, 38 decompViewDict) { 39 EmperorViewControllerABC.call(this, uiState, container, title, description); 40 if (decompViewDict === undefined) { 41 throw Error('The decomposition view dictionary cannot be undefined'); 42 } 43 for (var dv in decompViewDict) { 44 if (!dv instanceof DecompositionView) { 45 throw Error('The decomposition view dictionary ' + 46 'can only have decomposition views'); 47 } 48 } 49 if (_.size(decompViewDict) <= 0) { 50 throw Error('The decomposition view dictionary cannot be empty'); 51 } 52 53 /** 54 * @type {Object} 55 * This is object is keyed by unique identifiers and the values are 56 * DecompositionView objects referring to a set of objects presented on 57 * screen. This dictionary will usually be shared by all the tabs in the 58 * application. This argument is passed by reference. 59 */ 60 this.decompViewDict = decompViewDict; 61 62 /** 63 * @type {Function} 64 * Callback to execute when all the elements in the UI for this controller 65 * have been loaded. Note, that this functionality needs to be implemented 66 * by subclasses, as EmperorViewController does not have any UI components. 67 */ 68 this.ready = null; 69 70 return this; 71 } 72 EmperorViewController.prototype = Object.create( 73 EmperorViewControllerABC.prototype); 74 EmperorViewController.prototype.constructor = EmperorViewControllerABC; 75 76 /** 77 * 78 * Retrieve a view from the controller. 79 * 80 * This class does not operate on single decomposition views, hence this 81 * method retrieves the first available view. 82 * 83 */ 84 EmperorViewController.prototype.getView = function() { 85 // return the first decomposition view available in the dictionary 86 return this.decompViewDict[Object.keys(this.decompViewDict)[0]]; 87 }; 88 89 /** 90 * Check if a metadata field is present 91 * 92 * @param {String} m Metadata column to check if is present. 93 * 94 * @return {Bool} Whether or not the metadata field is present. 95 * 96 */ 97 EmperorViewController.prototype.hasMetadataField = function(m) { 98 // loop through the metadata headers in the decompositon views 99 // FIXME: There's no good way to specify the current decomposition name 100 // this needs to be added to the interface. 101 var res = _.find(this.decompViewDict, function(view) { 102 return view.decomp.md_headers.indexOf(m) !== -1; 103 }); 104 105 return res !== undefined; 106 }; 107 108 /** 109 * 110 * @class EmperorAttributeABC 111 * 112 * Initializes an abstract tab for attributes i.e. shape, color, size, etc. 113 * This has to be contained in a DOM object and will use the full size of 114 * that container. 115 * 116 * @param {UIState} uiState the shared state 117 * @param {Node} container Container node to create the controller in. 118 * @param {String} title title of the tab. 119 * @param {String} description helper description. 120 * @param {Object} decompViewDict This is object is keyed by unique 121 * identifiers and the values are DecompositionView objects referring to a 122 * set of objects presented on screen. This dictionary will usually be shared 123 * by all the tabs in the application. This argument is passed by reference. 124 * @param {Object} options This is a dictionary of options used to build 125 * the view controller. Used to set attributes of the slick grid and the 126 * metadata category drop down. At the moment the constructor only expects 127 * the following attributes: 128 * - categorySelectionCallback: a function object that's called when a new 129 * metadata category is selected in the dropdown living in the header. 130 * See [change]{@link https://api.jquery.com/change/}. 131 * - valueUpdatedCallback: a function object that's called when a metadata 132 * visualization attribute is modified (i.e. a change of color). 133 * See [onCellChange]{@link 134 * https://github.com/mleibman/SlickGrid/wiki/Grid-Events}. 135 * - slickGridColumn: a dictionary specifying options to be passed into the 136 * slickGrid. For instance, the ColorFormatter and the ColorEditor would be 137 * passed here. For more information, refer to the Slick Grid 138 * documentation. 139 * 140 * @return {EmperorAttributeABC} Returns an instance of the 141 * EmperorAttributeABC class. 142 * @constructs EmperorAttributeABC 143 * @extends EmperorViewController 144 * 145 */ 146 function EmperorAttributeABC(uiState, container, title, description, 147 decompViewDict, options) { 148 EmperorViewController.call(this, uiState, container, title, description, 149 decompViewDict); 150 151 /** 152 * @type {Object} 153 * Dictionary-like object where keys are metadata categories and values are 154 * lists of metadata columns. This object reflects the data presented in 155 * the metadata menu. 156 * @private 157 */ 158 this._metadata = {}; 159 160 /** 161 * @type {Node} 162 * jQuery element for the div containing the slickgrid of sample information 163 */ 164 this.$gridDiv = $('<div name="emperor-grid-div"></div>'); 165 this.$gridDiv.css('margin', '0 auto'); 166 this.$gridDiv.css('width', '100%'); 167 this.$gridDiv.css('height', '100%'); 168 this.$gridDiv.attr('title', 'Change the ' + title.toLowerCase() + ' with' + 169 ' the left column controls.'); 170 this.$body.append(this.$gridDiv); 171 172 var dm = this.getView().decomp; 173 var scope = this; 174 175 // http://stackoverflow.com/a/6602002 176 this.$select = $('<select>'); 177 this.$header.append(this.$select); 178 179 this.$searchBar = $("<input type='search' " + 180 "placeholder='Search for a value ...'>" 181 ).css({ 182 'width': '100%' 183 }); 184 this.$header.append(this.$searchBar); 185 186 // there's a few attributes we can only set on "ready" so list them up here 187 $(function() { 188 scope.$searchBar.tooltip({ 189 content: 'No results found!', 190 disabled: true, 191 // place the element with a slight offset at the bottom of the input 192 // so that it doesn't overlap with the "continuous values" elements 193 position: {my: 'center top+40', at: 'center bottom', 194 of: scope.$searchBar}, 195 // prevent the tooltip from disappearing when there's no matches 196 close: function(event, ui) { 197 if (scope.bodyGrid.getDataLength() === 0 && 198 scope.$searchBar.val() !== '') { 199 scope.$searchBar.tooltip('open'); 200 } 201 } 202 }); 203 204 var placeholder = 'Select a ' + scope.title + ' Category'; 205 206 // setup the slick grid 207 scope._buildGrid(options); 208 209 scope.refreshMetadata(); 210 211 // once this element is ready, it is safe to execute the "ready" callback 212 // if a subclass needs to wait on other elements, this attribute should 213 // be changed to null so this callback is effectively cancelled, for an 214 // example see the constructor of ColorViewController 215 scope.$select.on('chosen:ready', function() { 216 if (scope.ready !== null) { 217 scope.ready(); 218 } 219 }); 220 221 // setup chosen 222 scope.$select.chosen({width: '100%', search_contains: true, 223 include_group_label_in_selected: true, 224 placeholder_text_single: placeholder}); 225 226 // only subclasses will provide this callback 227 if (options.categorySelectionCallback !== undefined) { 228 229 // Disable interface controls (except the metadata selector) to 230 // prevent errors while no metadata category is selected. Once the 231 // user selects a metadata category, the controls will be enabled 232 // (see setSlickGridDataset). 233 scope.setEnabled(false); 234 scope.$select.val(''); 235 scope.$select.prop('disabled', false).trigger('chosen:updated'); 236 237 scope.$select.chosen().change(options.categorySelectionCallback); 238 } 239 240 // general events 241 scope._setupEvents(); 242 }); 243 244 return this; 245 } 246 EmperorAttributeABC.prototype = Object.create( 247 EmperorViewController.prototype); 248 EmperorAttributeABC.prototype.constructor = EmperorViewController; 249 250 /** 251 * 252 * Get the name of the decomposition selected in the metadata menu. 253 * 254 */ 255 EmperorAttributeABC.prototype.decompositionName = function(cat) { 256 return this.$select.find(':selected').parent().attr('label'); 257 }; 258 259 /** 260 * 261 * Get the view that's currently selected by the metadata menu. 262 * 263 */ 264 EmperorAttributeABC.prototype.getView = function() { 265 var view; 266 267 try { 268 view = this.decompViewDict[this.decompositionName()]; 269 } 270 catch (TypeError) { 271 view = EmperorViewController.prototype.getView.call(this); 272 } 273 274 return view; 275 }; 276 277 /** 278 * 279 * Private method to reset the attributes of the controller. 280 * 281 * Subclasses should implement this method as a way to reset the visual 282 * attributes of a given plot. 283 * @private 284 * 285 */ 286 EmperorAttributeABC.prototype._resetAttribute = function() { 287 }; 288 289 /** 290 * Changes the selected value in the metadata menu. 291 * 292 * @param {String} m Metadata column name to control. When the category is 293 * ``null``, the metadata selector is set to an empty value, the body grid 294 * is emptied, and all the markers are reset to a default state (depends on 295 * the subclass). 296 * 297 * @throws {Error} Argument `m` must be a metadata category in one of the 298 * decomposition views. 299 */ 300 EmperorAttributeABC.prototype.setMetadataField = function(m) { 301 if (m === null) { 302 this._resetAttribute(); 303 304 this.$select.val(''); 305 this.setSlickGridDataset([]); 306 307 this.setEnabled(false); 308 this.$select.prop('disabled', false).trigger('chosen:updated'); 309 310 return; 311 } 312 313 if (!this.hasMetadataField(m)) { 314 throw Error('Cannot set "' + m + '" as the metadata field, this column' + 315 ' is not available in the decomposition views'); 316 } 317 318 this.$select.val(m); 319 this.$select.trigger('chosen:updated'); 320 this.$select.change(); 321 }; 322 323 /** 324 * 325 * Get the name of the selected category in the metadata menu. 326 * 327 */ 328 EmperorAttributeABC.prototype.getMetadataField = function() { 329 return this.$select.val(); 330 }; 331 332 /** 333 * Retrieves the underlying data in the slick grid 334 * @return {Array} Returns an array of objects 335 * displayed by the body grid. 336 */ 337 EmperorAttributeABC.prototype.getSlickGridDataset = function() { 338 return this.bodyGrid.getData().getItems(); 339 }; 340 341 /** 342 * Changes the underlying data in the slick grid 343 * 344 * @param {Array} data data. 345 */ 346 EmperorAttributeABC.prototype.setSlickGridDataset = function(data) { 347 348 // Accounts for cases where controllers have not been set to a metadata 349 // category. In these cases all controllers (except for the metadata 350 // selector) are disabled to prevent interface errors. 351 if (this.getSlickGridDataset().length === 0 && this.enabled === false) { 352 this.setEnabled(true); 353 } 354 355 // Re-render the grid on the DOM 356 this.bodyGrid.getData().beginUpdate(); 357 this.bodyGrid.getData().setItems(data); 358 this.bodyGrid.getData().endUpdate(); 359 this.bodyGrid.invalidate(); 360 this.bodyGrid.render(); 361 }; 362 363 /** 364 * Method in charge of initializing the SlickGrid object 365 * 366 * @param {Object} [options] additional options to initialize the slick grid 367 * of this object. 368 * @private 369 * 370 */ 371 EmperorAttributeABC.prototype._buildGrid = function(options) { 372 var columns = [{id: 'field1', name: '', field: 'category'}], scope = this; 373 374 // autoEdit enables one-click editor trigger on the entire grid, instead 375 // of requiring users to click twice on a widget. 376 var gridOptions = {editable: true, enableAddRow: false, 377 enableCellNavigation: true, forceFitColumns: true, 378 enableColumnReorder: false, autoEdit: true}; 379 380 // If there's a custom slickgrid column then add it to the object 381 if (options.slickGridColumn !== undefined) { 382 columns.unshift(options.slickGridColumn); 383 } 384 385 var dataView = new Slick.Data.DataView(), searchString = ''; 386 387 /** 388 * @type {Slick.Grid} 389 * Container that lists the metadata categories described under the 390 * metadata column and the attribute that can be modified. 391 */ 392 this.bodyGrid = new Slick.Grid(this.$gridDiv, dataView, columns, 393 gridOptions); 394 395 this.$searchBar.on('input', function(e) { 396 dataView.refresh(); 397 398 // show a message when no results are found 399 if (scope.bodyGrid.getDataLength() === 0 && 400 scope.$searchBar.val() !== '') { 401 scope.$searchBar.tooltip('option', 'disabled', false); 402 scope.$searchBar.tooltip('open'); 403 } 404 else { 405 scope.$searchBar.tooltip('option', 'disabled', true); 406 scope.$searchBar.tooltip('close'); 407 } 408 409 }); 410 411 function substringFilter(item, args) { 412 var val = scope.$searchBar.val(); 413 if (!searchString && val && 414 item.category.toLowerCase().indexOf(val.toLowerCase()) === -1) { 415 return false; 416 } 417 return true; 418 } 419 420 dataView.onRowCountChanged.subscribe(function(e, args) { 421 scope.bodyGrid.updateRowCount(); 422 scope.bodyGrid.render(); 423 }); 424 425 dataView.onRowsChanged.subscribe(function(e, args) { 426 scope.bodyGrid.invalidateRows(args.rows); 427 scope.bodyGrid.render(); 428 }); 429 430 dataView.setFilter(substringFilter); 431 432 // hide the header row of the grid 433 // http://stackoverflow.com/a/29827664/379593 434 $(this.$body).find('.slick-header').css('display', 'none'); 435 436 // subscribe to events when a cell is changed 437 this.bodyGrid.onCellChange.subscribe(options.valueUpdatedCallback); 438 }; 439 440 EmperorAttributeABC.prototype._setupEvents = function() { 441 var scope = this; 442 443 // dispatch an event when the category changes 444 this.$select.on('change', function() { 445 scope.dispatchEvent({type: 'category-changed', 446 message: {category: scope.getMetadataField(), 447 controller: scope} 448 }); 449 }); 450 451 // dispatch an event when a value changes and send the plottable objects 452 this.bodyGrid.onCellChange.subscribe(function(e, args) { 453 scope.dispatchEvent({type: 'value-changed', 454 message: {category: scope.getMetadataField(), 455 attribute: args.item.value, 456 group: args.item.plottables, 457 controller: scope} 458 }); 459 }); 460 461 // dispatch an event when a category is double-clicked 462 this.bodyGrid.onDblClick.subscribe(function(e, args) { 463 var item = scope.bodyGrid.getDataItem(args.row); 464 scope.dispatchEvent({type: 'value-double-clicked', 465 message: {category: scope.getMetadataField(), 466 value: item.category, 467 attribute: item.value, 468 group: item.plottables, 469 controller: scope} 470 }); 471 }); 472 }; 473 474 /** 475 * Resizes the container and the individual elements. 476 * 477 * Note, the consumer of this class, likely the main controller should call 478 * the resize function any time a resizing event happens. 479 * 480 * @param {Float} width the container width. 481 * @param {Float} height the container height. 482 */ 483 EmperorAttributeABC.prototype.resize = function(width, height) { 484 // call super, most of the header and body resizing logic is done there 485 EmperorViewController.prototype.resize.call(this, width, height); 486 487 // the whole code is asynchronous, so there may be situations where 488 // bodyGrid doesn't exist yet, so check before trying to modify the object 489 if (this.bodyGrid !== undefined) { 490 // make the columns fit the available space whenever the window resizes 491 // http://stackoverflow.com/a/29835739 492 this.bodyGrid.setColumns(this.bodyGrid.getColumns()); 493 // Resize the slickgrid canvas for the new body size. 494 this.bodyGrid.resizeCanvas(); 495 } 496 }; 497 498 /** 499 * Converts the current instance into a JSON object. 500 * 501 * @return {Object} base object ready for JSON conversion. 502 */ 503 EmperorAttributeABC.prototype.toJSON = function() { 504 var json = {}; 505 json.category = this.getMetadataField(); 506 507 // Convert SlickGrid list of objects to single object 508 var gridData = this.getSlickGridDataset(); 509 var jsonData = {}; 510 for (var i = 0; i < gridData.length; i++) { 511 jsonData[gridData[i].category] = gridData[i].value; 512 } 513 json.data = jsonData; 514 return json; 515 }; 516 517 /** 518 * Decodes JSON string and modifies its own instance variables accordingly. 519 * 520 * @param {Object} json Parsed JSON string representation of self. 521 * 522 */ 523 EmperorAttributeABC.prototype.fromJSON = function(json) { 524 this.setMetadataField(json.category); 525 526 // if the category is null, then we just reset the controller 527 if (json.category !== null) { 528 // fetch and set the SlickGrid-formatted data 529 var data = this.getView().setCategory( 530 json.data, this.setPlottableAttributes, json.category); 531 this.setSlickGridDataset(data); 532 // set all to needsUpdate 533 this.getView().needsUpdate = true; 534 } 535 }; 536 537 /** 538 * 539 * Update the metadata selection menu. 540 * 541 * Performs some additional logic to avoid duplicating decomposition names. 542 * 543 * Note that decompositions won't be updated if they have the same name and 544 * same metadata headers, if the only things changing are coordinates, or 545 * metadata values, the changes should be performed directly on the objects 546 * themselves. 547 * 548 */ 549 EmperorAttributeABC.prototype.refreshMetadata = function() { 550 var scope = this, group, hdrs; 551 552 _.each(this.decompViewDict, function(view, name) { 553 // sort alphabetically the metadata headers ( 554 hdrs = _.sortBy(view.decomp.md_headers, function(x) { 555 return x.toLowerCase(); 556 }); 557 558 // Before we update the metadata view, we rectify that we don't have that 559 // information already. The order in this conditional matters as we hope 560 // to short-circuit if the name is not already present. If that's not 561 // the case, we also check to ensure the lists are equivalent. 562 if (_.contains(_.keys(scope._metadata), name) && 563 _.intersection(scope._metadata[name], hdrs).length == hdrs.length && 564 scope._metadata[name].length == hdrs.length) { 565 return; 566 } 567 568 // create the new category 569 scope._metadata[name] = []; 570 571 group = $('<optgroup>').attr('label', name); 572 573 scope.$select.append(group); 574 575 _.each(hdrs, function(header) { 576 group.append($('<option>').attr('value', header).text(header)); 577 scope._metadata[name].push(header); 578 }); 579 }); 580 581 this.$select.trigger('chosen:updated'); 582 }; 583 584 /** 585 * Sets whether or not the tab can be modified or accessed. 586 * 587 * @param {Boolean} trulse option to enable tab. 588 */ 589 EmperorAttributeABC.prototype.setEnabled = function(trulse) { 590 EmperorViewController.prototype.setEnabled.call(this, trulse); 591 592 this.$select.prop('disabled', !trulse).trigger('chosen:updated'); 593 this.bodyGrid.setOptions({editable: trulse}); 594 this.$searchBar.prop('disabled', !trulse); 595 this.$searchBar.prop('hidden', !trulse); 596 }; 597 598 /** 599 * @class ScalarViewControllerABC 600 * 601 * Alters the scale of points displayed on the screen. 602 * 603 * @param {UIState} uiState The shared state 604 * @param {Node} container Container node to create the controller in. 605 * @param {String} title The name/title of the tab. 606 * @param {String} helpmenu description helper description. 607 * @param {Float} min Minimum value for the attribute. 608 * @param {Float} max Maximum value for the attribute. 609 * @param {Float} step Size of the step for an attribute slider. 610 * @param {Object} decompViewDict This object is keyed by unique identifiers 611 * and the values are DecompositionView objects referring to a set of objects 612 * presented on screen. This dictionary will usually be shared by all the 613 * tabs in the application. This argument is passed by reference. 614 * 615 * @return {ScalarViewControllerABC} 616 * @constructs ScalarViewControllerABC 617 * @extends EmperorAttributeABC 618 * 619 **/ 620 function ScalarViewControllerABC(uiState, container, title, helpmenu, min, 621 max, step, decompViewDict) { 622 // Create checkbox for scaling by values 623 /** 624 * jQuery node for checkbox controlling whether to scale by values or not 625 * @type {Node} 626 */ 627 this.$scaledValue = $('<input type="checkbox">'); 628 /** 629 * jQuery node for label of $scaledValues 630 * @type {Node} 631 */ 632 this.$scaledLabel = $('<label>Change ' + title.toLowerCase() + ' by ' + 633 'values</label>'); 634 this.$scaledLabel.attr('title', 'Samples with lower values will have ' + 635 'a decreased ' + title.toLowerCase()); 636 637 //Create global scale bar 638 /** 639 * jQuery node for global scale bar container div 640 * @type {Node} 641 */ 642 this.$globalDiv = $('<div style="width:100%;padding:5px;">'); 643 this.$globalDiv.html('<p>Global Scaling</p>'); 644 var $sliderDiv = $('<div style="width:80%;display:inline-block;">'); 645 var $viewval = $('<input type="text" value="1.0" readonly ' + 646 'style="border:0;width:25px;' + 647 'background-color:rgb(238, 238, 238)">'); 648 /** 649 * jQuery node for global scale bar 650 * @type {Node} 651 */ 652 this.$sliderGlobal = $sliderDiv.slider({ 653 range: 'max', 654 min: min, 655 max: max, 656 value: 1.0, 657 step: step, 658 slide: function(event, ui) { 659 $viewval.val(ui.value); 660 }, 661 stop: function(event, ui) { 662 // Update the slickgrid values with the new scalar 663 var data = scope.getSlickGridDataset(); 664 _.each(data, function(element) { 665 element.value = ui.value; 666 }); 667 scope.setSlickGridDataset(data); 668 scope.setAllPlottableAttributes(ui.value); 669 } 670 }); 671 this.$globalDiv.append($viewval); 672 this.$globalDiv.append($sliderDiv); 673 674 // Constant for width in slick-grid 675 var SLICK_WIDTH = 50, scope = this; 676 677 // Build the options dictionary 678 var options = { 679 'valueUpdatedCallback': function(e, args) { 680 var scalar = +args.item.value; 681 var group = args.item.plottables; 682 var element = scope.getView(); 683 scope.setPlottableAttributes(element, scalar, group); 684 }, 685 'categorySelectionCallback': function(evt, params) { 686 var category = scope.$select.val(); 687 var decompViewDict = scope.getView(); 688 var attributes; 689 690 // getting all unique values per categories 691 var uniqueVals = decompViewDict.decomp.getUniqueValuesByCategory( 692 category); 693 // getting a scalar value for each point 694 var scaled = scope.$scaledValue.is(':checked'); 695 try { 696 attributes = scope.getScale(uniqueVals, scaled); 697 } 698 catch (err) { 699 scope.$scaledValue.attr('checked', false); 700 return; 701 } 702 if (scaled) { 703 scope.$globalDiv.hide(); 704 } 705 else { 706 scope.$globalDiv.show(); 707 } 708 scope.resize(); 709 710 // fetch the slickgrid-formatted data 711 var data = decompViewDict.setCategory(attributes, 712 scope.setPlottableAttributes, 713 category); 714 715 scope.setSlickGridDataset(data); 716 717 scope.$sliderGlobal.slider('value', 1); 718 $viewval.val(1); 719 }, 720 'slickGridColumn': {id: 'title', name: title, field: 'value', 721 sortable: false, maxWidth: SLICK_WIDTH, 722 minWidth: SLICK_WIDTH, 723 editor: ScaleEditor.ScaleEditor, 724 formatter: ScaleEditor.ScaleFormatter}, 725 'editorOptions': {'min': min, 'max': max, 'step': step} 726 }; 727 728 EmperorAttributeABC.call(this, uiState, container, title, helpmenu, 729 decompViewDict, options); 730 731 this.$header.append(this.$scaledValue); 732 this.$header.append(this.$scaledLabel); 733 this.$body.prepend(this.$globalDiv); 734 735 scope.$scaledValue.on('change', options.categorySelectionCallback); 736 737 return this; 738 } 739 ScalarViewControllerABC.prototype = Object.create( 740 EmperorAttributeABC.prototype); 741 ScalarViewControllerABC.prototype.constructor = EmperorAttributeABC; 742 743 /** 744 * Converts the current instance into a JSON string. 745 * 746 * @return {Object} JSON ready representation of self. 747 */ 748 ScalarViewControllerABC.prototype.toJSON = function() { 749 var json = EmperorAttributeABC.prototype.toJSON.call(this); 750 json.globalScale = this.$globalDiv.children('input').val(); 751 json.scaleVal = this.$scaledValue.is(':checked'); 752 return json; 753 }; 754 755 /** 756 * Decodes JSON string and modifies its own instance variables accordingly. 757 * 758 * @param {Object} Parsed JSON string representation of self. 759 */ 760 ScalarViewControllerABC.prototype.fromJSON = function(json) { 761 // Can't call super because select needs to be set first Order here is 762 // important. We want to set all the extra controller settings before we 763 // load from json, as they can override the JSON when set 764 765 this.setMetadataField(json.category); 766 767 // if the category is null, then there's nothing to set about the state 768 // of the controller 769 if (json.category === null) { 770 return; 771 } 772 773 this.$select.val(json.category); 774 this.$select.trigger('chosen:updated'); 775 this.$sliderGlobal.slider('value', json.globalScale); 776 this.$scaledValue.prop('checked', json.scaleVal); 777 this.$scaledValue.trigger('change'); 778 779 // fetch and set the SlickGrid-formatted data 780 var data = this.getView().setCategory(json.data, 781 this.setPlottableAttributes, 782 json.category); 783 this.setSlickGridDataset(data); 784 785 // set all to needsUpdate 786 this.getView().needsUpdate = true; 787 }; 788 789 /** 790 * Resizes the container and the individual elements. 791 * 792 * Note, the consumer of this class, likely the main controller should call 793 * the resize function any time a resizing event happens. 794 * 795 * @param {float} width the container width. 796 * @param {float} height the container height. 797 */ 798 ScalarViewControllerABC.prototype.resize = function(width, height) { 799 this.$body.height(this.$canvas.height() - this.$header.height()); 800 this.$body.width(this.$canvas.width()); 801 802 //scale gridDiv based on whether global scaling available or not 803 if (this.$scaledValue.is(':checked')) { 804 this.$gridDiv.css('height', '100%'); 805 } 806 else { 807 this.$gridDiv.css( 808 'height', this.$body.height() - this.$globalDiv.height() - 10); 809 } 810 811 // call super, most of the header and body resizing logic is done there 812 EmperorAttributeABC.prototype.resize.call(this, width, height); 813 }; 814 815 /** 816 * Sets whether or not elements in the tab can be modified. 817 * 818 * @param {Boolean} trulse option to enable elements. 819 */ 820 ScalarViewControllerABC.prototype.setEnabled = function(trulse) { 821 EmperorAttributeABC.prototype.setEnabled.call(this, trulse); 822 823 var color; 824 825 this.$scaledValue.prop('disabled', !trulse); 826 this.$sliderGlobal.slider('option', 'disabled', !trulse); 827 828 if (trulse) { 829 color = '#70caff'; 830 } 831 else { 832 color = ''; 833 } 834 this.$sliderGlobal.css('background', color); 835 }; 836 837 /** 838 * 839 * Private method to reset the scale of all the objects to one. 840 * 841 * @extends EmperorAttributeABC 842 * @private 843 * 844 */ 845 ScalarViewControllerABC.prototype._resetAttribute = function() { 846 EmperorAttributeABC.prototype._resetAttribute.call(this); 847 848 var scope = this; 849 this.$scaledValue.prop('checked', false); 850 851 _.each(this.decompViewDict, function(view) { 852 scope.setPlottableAttributes(view, 1, view.decomp.plottable); 853 view.needsUpdate = true; 854 }); 855 }; 856 857 /** 858 * 859 * Helper function to set the scale of plottable. 860 * 861 * Note, needs to be overriden by the subclass. 862 * 863 */ 864 ScalarViewControllerABC.prototype.setPlottableAttributes = function() { 865 }; 866 867 /** 868 * 869 * Method to do global updates to only the current view 870 * 871 * Note, needs to be overriden by the subclass. 872 * 873 */ 874 ScalarViewControllerABC.prototype.setAllPlottableAttributes = function() { 875 }; 876 877 /** 878 * 879 * Scaling function to use when the attribute is based on a metadata 880 * category (used in getScale). 881 * 882 * @param {float} val The metadata value for the current sample. 883 * @param {float} min The minimum metadata value in the dataset. 884 * @param {float} range The span of the metadata values. 885 * 886 * @return {float} Attribute value 887 * 888 */ 889 ScalarViewControllerABC.prototype.scaleValue = function(val, min, range) { 890 return 1; 891 }; 892 893 /** 894 * Helper function to get the scale for each metadata value 895 * 896 * @param {String[]} values The values to get scale for 897 * @param {Boolean} scaled Whether or not to scale by values or just reset to 898 * standard scale (1.0) 899 * 900 * @throws {Error} No or one numeric value in category and trying to scale by 901 * value 902 */ 903 ScalarViewControllerABC.prototype.getScale = function(values, scaled) { 904 var scale = {}, numbers, val, scope = this; 905 906 if (!scaled) { 907 _.each(values, function(element) { 908 scale[element] = 1.0; 909 }); 910 } 911 else { 912 //See if we have numeric values, fail if no 913 var split = util.splitNumericValues(values); 914 915 if (split.numeric.length < 2) { 916 alert('Not enough numeric values in category, can not scale by value!'); 917 throw new Error('no numeric values'); 918 } 919 920 // Alert if we have non-numerics and scale them to 0 921 if (split.nonNumeric.length > 0) { 922 _.each(split.nonNumeric, function(element) { 923 scale[element] = 0.0; 924 }); 925 alert('Non-numeric values detected. These will be hidden!'); 926 } 927 928 // convert objects to numbers so we can map them to a color, we keep a 929 // copy of the untransformed object so we can search the metadata 930 numbers = _.map(split.numeric, parseFloat); 931 932 //scale remaining values between 1 and 5 scale 933 var min = _.min(numbers); 934 var max = _.max(numbers); 935 var range = max - min; 936 937 _.each(split.numeric, function(element) { 938 // note these elements are not numbers 939 val = parseFloat(element); 940 941 // Scale the values, then round to 4 decimal places. 942 scale[element] = scope.scaleValue(val, min, range); 943 }); 944 } 945 return scale; 946 }; 947 948 return {'EmperorViewControllerABC': EmperorViewControllerABC, 949 'EmperorViewController': EmperorViewController, 950 'EmperorAttributeABC': EmperorAttributeABC, 951 'ScalarViewControllerABC': ScalarViewControllerABC}; 952 }); 953