From b082c16df7cea2cc27fbe6c0d14c106a53409229 Mon Sep 17 00:00:00 2001 From: Sascha Herzinger Date: Thu, 21 Apr 2016 09:07:04 +0200 Subject: [PATCH 1/6] lots of refactoring and code cleaning, also squished some bugs in the process --- .jshintrc | 5 +- grails-app/views/layouts/_heatmap.gsp | 2 +- .../javascript/smartRUtilsServiceTests.js | 3 +- .../js/smartR/_angular/controllers/heatmap.js | 2 +- .../smartR/_angular/directives/fetchButton.js | 30 +- .../_angular/directives/preprocessButton.js | 134 ++-- .../smartR/_angular/directives/runButton.js | 57 +- .../services/commonWorkflowService.js | 19 +- .../smartR/_angular/services/rServeService.js | 672 ++++++++---------- .../smartR/_angular/services/smartRUtils.js | 12 +- 10 files changed, 426 insertions(+), 510 deletions(-) diff --git a/.jshintrc b/.jshintrc index 08b4125..0bf30b8 100644 --- a/.jshintrc +++ b/.jshintrc @@ -45,7 +45,7 @@ "evil" : false, // true: Tolerate use of `eval` and `new Function()` "expr" : false, // true: Tolerate `ExpressionStatement` as Programs "funcscope" : false, // true: Tolerate defining variables inside control statements - + "globalstrict" : true, // true: Allow global "use strict" (also enables 'strict') "iterator" : false, // true: Tolerate using the `__iterator__` property "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block @@ -89,6 +89,7 @@ "angular": true, "module": true, "inject": true, - "pageInfo": true + "pageInfo": true, + "d3": true } // additional predefined global variables } diff --git a/grails-app/views/layouts/_heatmap.gsp b/grails-app/views/layouts/_heatmap.gsp index 7bb8f22..80aa08f 100644 --- a/grails-app/views/layouts/_heatmap.gsp +++ b/grails-app/views/layouts/_heatmap.gsp @@ -42,7 +42,7 @@ %{--Aggregate Probes--}%
- + Aggregate probes
diff --git a/test/unit/javascript/smartRUtilsServiceTests.js b/test/unit/javascript/smartRUtilsServiceTests.js index f7ec4fc..96e64a5 100644 --- a/test/unit/javascript/smartRUtilsServiceTests.js +++ b/test/unit/javascript/smartRUtilsServiceTests.js @@ -1,5 +1,6 @@ +'use strict'; + describe('smartRUtils', function() { - 'use strict'; var smartRUtils; diff --git a/web-app/js/smartR/_angular/controllers/heatmap.js b/web-app/js/smartR/_angular/controllers/heatmap.js index 5162c33..de150f9 100644 --- a/web-app/js/smartR/_angular/controllers/heatmap.js +++ b/web-app/js/smartR/_angular/controllers/heatmap.js @@ -30,7 +30,7 @@ window.smartRApp.controller('HeatmapController', disabled: true, running: false, params: { - aggregateProbes: false + aggregate: false }, scriptResults: {} }; diff --git a/web-app/js/smartR/_angular/directives/fetchButton.js b/web-app/js/smartR/_angular/directives/fetchButton.js index 9aea1e2..7beb031 100644 --- a/web-app/js/smartR/_angular/directives/fetchButton.js +++ b/web-app/js/smartR/_angular/directives/fetchButton.js @@ -21,7 +21,6 @@ window.smartRApp.directive('fetchButton', [ }, templateUrl: $rootScope.smartRPath + '/js/smartR/_angular/templates/fetchButton.html', link: function(scope, element) { - var template_btn = element.children()[0], template_msg = element.children()[1]; @@ -66,19 +65,13 @@ window.smartRApp.directive('fetchButton', [ } }; - var _afterDataFetched = function() { - if (!scope.showSummaryStats) { - _onSuccess(); - } else { - template_msg.innerHTML = - 'Execute summary statistics, please wait _'; - - rServeService.executeSummaryStats('fetch') - .then( - function(data) { scope.summaryData = data.result; }, // this will trigger $watch - _onFailure - ); - } + var _showSummaryStats = function() { + template_msg.innerHTML = 'Execute summary statistics, please wait _'; + rServeService.executeSummaryStats('fetch') + .then( + function(data) { scope.summaryData = data.result; }, // this will trigger $watch + _onFailure + ); }; template_btn.onclick = function() { @@ -92,20 +85,23 @@ window.smartRApp.directive('fetchButton', [ template_msg.innerHTML = 'Fetching data, please wait _'; if (smartRUtils.countCohorts() === 0) { - _onFailure('No cohorts selected!'); + _onFailure('Error: No cohorts selected!'); return; } var conceptKeys = smartRUtils.conceptBoxMapToConceptKeys(scope.conceptMap); if ($.isEmptyObject(conceptKeys)) { - _onFailure('No concepts selected!'); + _onFailure('Error: No concepts selected!'); return; } var dataConstraints = _getDataConstraints(scope.biomarkers); rServeService.loadDataIntoSession(conceptKeys, dataConstraints) - .then(_afterDataFetched, _onFailure); + .then( + scope.showSummaryStats ? _showSummaryStats : _onSuccess, + _onFailure + ); }; } }; diff --git a/web-app/js/smartR/_angular/directives/preprocessButton.js b/web-app/js/smartR/_angular/directives/preprocessButton.js index 2462cec..cce3f2b 100644 --- a/web-app/js/smartR/_angular/directives/preprocessButton.js +++ b/web-app/js/smartR/_angular/directives/preprocessButton.js @@ -1,78 +1,60 @@ //# sourceURL=preprocessButton.js -window.smartRApp.directive('preprocessButton', ['rServeService', - function(rServeService) { - return { - restrict: 'E', - scope: { - running: '=', - params: '=', - showSummaryStats: '=', - summaryData: '=' - }, - template: - '' + - '', - link: function(scope, element) { - var template_btn = element.children()[0]; - var template_msg = element.children()[1]; - - scope.$watch('summaryData', function (newSummaryData) { - if (newSummaryData.hasOwnProperty('allSamples')) { - // when everything is retrieved - scope.disabled = false; - } - }, true); - - template_btn.onclick = function() { - - var _init = function () { - template_btn.disabled = true; - scope.summaryData = {}; // reset - scope.disabled = true; - scope.running = true; - template_msg.innerHTML = 'Preprocessing, please wait _'; - }, - - _args = {aggregate:scope.params.aggregateProbes}, - - _preprocessData = function (_args) { - return rServeService.preprocess(_args).then(function (msg){ - return msg; - }); - }, - - _finishedPreprocessed = function (msg) { - template_msg.innerHTML = 'Success: ' + msg; - scope.disabled = false; - scope.running = false; - }, - - _afterDataPreprocessed = function (msg) { - template_btn.disabled = false; - - if (!scope.showSummaryStats) { - return _finishedPreprocessed(msg); - } - template_msg.innerHTML = 'Execute summary statistics, please wait _'; - - return rServeService.executeSummaryStats('preprocess') - .then (function (data) { - scope.summaryData = data.result; - template_msg.innerHTML = 'Success: ' + data.msg; - scope.running = false; - }, function(msg) { - template_msg.innerHTML = 'Failure: ' + msg; - scope.running = false; - }) - }; - - _init(); - - _preprocessData(_args) - .then(_afterDataPreprocessed, _afterDataPreprocessed); - - }; - } - }; -}]); +'use strict'; + +window.smartRApp.directive('preprocessButton', [ + 'rServeService', + '$rootScope', + function(rServeService, $rootScope) { + return { + restrict: 'E', + scope: { + running: '=?', + params: '=?', + showSummaryStats: '=', + summaryData: '=' + }, + templateUrl: $rootScope.smartRPath + '/js/smartR/_angular/templates/preprocessButton.html', + link: function(scope, element) { + + var template_btn = element.children()[0]; + var template_msg = element.children()[1]; + + var _onSuccess = function() { + template_msg.innerHTML = 'Task complete!'; + template_btn.disabled = false; + scope.running = false; + }; + + var _onFail = function(msg) { + template_msg.innerHTML = 'Error: ' + msg; + template_btn.disabled = false; + scope.running = false; + }; + + var _showSummaryStats = function() { + template_msg.innerHTML = 'Execute summary statistics, please wait _'; + rServeService.executeSummaryStats('preprocess').then( + function (data) { + scope.summaryData = data.result; + _onSuccess(); + }, + _onFail + ); + }; + + template_btn.onclick = function() { + scope.summaryData = {}; + scope.disabled = true; + scope.running = true; + template_msg.innerHTML = 'Preprocessing, please wait _'; + + var params = scope.params ? scope.params : {}; + rServeService.preprocess(params).then( + scope.showSummaryStats ? _showSummaryStats : _onSuccess, + _onFail + ); + }; + } + }; + }]); diff --git a/web-app/js/smartR/_angular/directives/runButton.js b/web-app/js/smartR/_angular/directives/runButton.js index aa0dd4a..9d47472 100644 --- a/web-app/js/smartR/_angular/directives/runButton.js +++ b/web-app/js/smartR/_angular/directives/runButton.js @@ -2,49 +2,52 @@ 'use strict'; -window.smartRApp.directive('runButton', - ['$rootScope', 'rServeService', function($rootScope, rServeService) { +window.smartRApp.directive('runButton', [ + '$rootScope', + 'rServeService', + function($rootScope, rServeService) { return { restrict: 'E', scope: { - running: '=', + running: '=?', storage: '=storeResultsIn', script: '@scriptToRun', name: '@buttonName', - serialized: '=', - arguments: '=argumentsToUse' + serialized: '=?', + params: '=?argumentsToUse' }, templateUrl: $rootScope.smartRPath + '/js/smartR/_angular/templates/runButton.html', link: function(scope, element) { - var template_btn = element.children()[0], - template_msg = element.children()[1], - serialized = scope.serialized; + template_msg = element.children()[1]; - var _downloadData = function(response) { - template_msg.innerHTML = ''; // empty template - if (serialized) { // when results are serialized, we need to deserialized them by - // downloading the results files. - rServeService.downloadJsonFile(response.executionId, 'heatmap.json').then( - function(d) { scope.storage = d.data; _done(); }, - _onFail - ); - } else { // results - scope.storage = JSON.parse(response.result.artifacts.value); - _done(); - } + var _onSuccess = function(data) { + scope.storage = data; + template_msg.innerHTML = ''; + template_btn.disabled = false; + scope.disabled = false; + scope.running = false; }; - var _done = function() { + var _onFail = function(msg) { + template_msg.innerHTML = 'Error: ' + msg; template_btn.disabled = false; scope.disabled = false; scope.running = false; }; - var _onFail = function(response) { - template_msg.style.color = 'red'; - template_msg.innerHTML = 'Failure: ' + response.statusText; - _done(); + var _prepareResults = function(response) { + if (scope.serialized) { + console.log('hi') + // when results are serialized, we need to deserialized them by + // downloading the results files. + rServeService.downloadJsonFile(response.executionId, 'heatmap.json').then( + function(d) { _onSuccess(d.data); }, + _onFail + ); + } else { + _onSuccess(JSON.parse(response.result.artifacts.value)); + } }; template_btn.onclick = function() { @@ -56,9 +59,9 @@ window.smartRApp.directive('runButton', rServeService.startScriptExecution({ taskType: scope.script, - arguments: scope.arguments + arguments: scope.params ? scope.params : {} }).then( - _downloadData, + _prepareResults, _onFail ); }; diff --git a/web-app/js/smartR/_angular/services/commonWorkflowService.js b/web-app/js/smartR/_angular/services/commonWorkflowService.js index 7cb1cf1..5586e8f 100644 --- a/web-app/js/smartR/_angular/services/commonWorkflowService.js +++ b/web-app/js/smartR/_angular/services/commonWorkflowService.js @@ -1,26 +1,11 @@ //# sourceURL=commonWorkflowService.js +'use strict'; + window.smartRApp.factory('commonWorkflowService', ['rServeService', '$css', function(rServeService, $css) { var service = {}; - var fetchFromObject = function (obj, prop){ - //property not found - if(typeof obj === 'undefined') return false; - - //index of next property split - var _index = prop.indexOf('.'); - - //property split found; recursive call - if(_index > -1){ - //get object at property (before split), pass on remainder - return fetchFromObject(obj[prop.substring(0, _index)], prop.substr(_index+1)); - } - - //no split; get property - return obj[prop]; - }; - service.initializeWorkflow = function(workflowName, scope) { service.currentScope = scope; // load workflow specific css diff --git a/web-app/js/smartR/_angular/services/rServeService.js b/web-app/js/smartR/_angular/services/rServeService.js index fea23df..8376ee6 100644 --- a/web-app/js/smartR/_angular/services/rServeService.js +++ b/web-app/js/smartR/_angular/services/rServeService.js @@ -1,406 +1,350 @@ //# sourceURL=rServeService.js -window.smartRApp.factory('rServeService', ['smartRUtils', '$q', '$http', function(smartRUtils, $q, $http) { - - var service = {}; - - var NOOP_ABORT = function() {}; - var TIMEOUT = 10000 /* 10 s */; - var CHECK_DELAY = 1000; - var SESSION_TOUCH_DELAY = 9 * 60 * 1000; /* 9 min; session timeout is 10 */ - - /* we only support one session at a time */ - - var state = { - currentRequestAbort: NOOP_ABORT, - sessionId: null, - touchTimeout: null // for current session id - }; - - var workflow = ''; - /* returns a promise with the session id and - * saves the session id for future calls */ - service.startSession = function(name) { - workflow = name; - return $http({ - url: pageInfo.basePath + '/RSession/create', - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - config: { - timeout: TIMEOUT - }, - data: { - workflow: workflow - } - }) - .then(function(response) { - state.sessionId = response.data.sessionId; - rServeService_scheduleTouch(); - }, transformAjaxFailure); +'use strict'; - }; +window.smartRApp.factory('rServeService', [ + 'smartRUtils', + '$q', + '$http', + function(smartRUtils, $q, $http) { - service.touch = function(sessionId) { - if (sessionId != state.sessionId) { - return; - } + var service = {}; + + var NOOP_ABORT = function() {}; + var TIMEOUT = 10000 /* 10 s */; + var CHECK_DELAY = 1000; + var SESSION_TOUCH_DELAY = 9 * 60 * 1000; /* 9 min; session timeout is 10 */ + + /* we only support one session at a time */ + + var state = { + currentRequestAbort: NOOP_ABORT, + sessionId: null, + touchTimeout: null // for current session id + }; + + var workflow = ''; + /* returns a promise with the session id and + * saves the session id for future calls */ + service.startSession = function(name) { + workflow = name; + var request = $http({ + url: pageInfo.basePath + '/RSession/create', + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + config: { + timeout: TIMEOUT + }, + data: { + workflow: workflow + } + }); - return $http({ - url: pageInfo.basePath + '/RSession/touch', - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - config: { - timeout: TIMEOUT - }, - data: { - sessionId: sessionId + return $q(function(resolve, reject) { + request.then( + function(response) { + state.sessionId = response.data.sessionId; + rServeService_scheduleTouch(); + resolve(); + }, + function(response) { reject(response.statusText);} + ); + }); + }; + + service.touch = function(sessionId) { + if (sessionId !== state.sessionId) { + return; } - }) - .finally(function() { + + var touchRequest = $http({ + url: pageInfo.basePath + '/RSession/touch', + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + config: { + timeout: TIMEOUT + }, + data: { + sessionId: sessionId + } + }); + + touchRequest.finally(function() { rServeService_scheduleTouch(); // schedule another - }) - }; - - function rServeService_scheduleTouch() { - var sessionId = state.sessionId; - window.clearTimeout(state.touchTimeout); - state.touchTimeout = window.setTimeout(function() { - service.touch(sessionId); - }, SESSION_TOUCH_DELAY); - } - - service.destroySession = function(sessionId) { - sessionId = sessionId || state.sessionId; - - if (!sessionId) { - return; + }); + }; + + function rServeService_scheduleTouch() { + var sessionId = state.sessionId; + window.clearTimeout(state.touchTimeout); + state.touchTimeout = window.setTimeout(function() { + service.touch(sessionId); + }, SESSION_TOUCH_DELAY); } - return $http({ - url: pageInfo.basePath + '/RSession/delete', - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - config: { - timeout: TIMEOUT - }, - data: { - sessionId: sessionId + service.destroySession = function(sessionId) { + sessionId = sessionId || state.sessionId; + + if (!sessionId) { + return; } - }) - .catch(transformAjaxFailure) - .finally(function() { - if (state.sessionId == sessionId) { - service.abandonCurrentSession(); + + var request = $http({ + url: pageInfo.basePath + '/RSession/delete', + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + config: { + timeout: TIMEOUT + }, + data: { + sessionId: sessionId } }); - }; - service.abandonCurrentSession = function() { - window.clearTimeout(state.touchTimeout); - state.sessionId = null; - }; + return request.finally(function() { + if (state.sessionId === sessionId) { + service.abandonCurrentSession(); + } + }); + }; - service.destroyAndStartSession = function(workflowName) { + service.abandonCurrentSession = function() { + window.clearTimeout(state.touchTimeout); + state.sessionId = null; + }; - // delete session before creating a new one - $q.when(service.destroySession()) - .then(function() { - // start a new session + service.destroyAndStartSession = function(workflowName) { + $q.when(service.destroySession()).then(function() { service.startSession(workflowName); }); + }; - }; - - /* - * taskData = { - * arguments: { ... }, - * taskType: 'fetchData' or name of R script minus .R, - * phase: 'fetch' | 'preprocess' | 'run', - * } - * - * If it succeeds, the result will be the data returned by the server. - * If it fails, it will return an object with at least these fields: - * { - * status: 0 | , - * statusText: , - * } - */ - service.startScriptExecution = function(taskDataOrig) { - - var taskData = $.extend({}, taskDataOrig); // clone the thing - state.currentRequestAbort(); - - var runRequest = $http({ - url: pageInfo.basePath + '/ScriptExecution/run', - method: 'POST', - timeout: TIMEOUT, - responseType: 'json', - data: JSON.stringify({ - sessionId: state.sessionId, - arguments: taskData.arguments, - taskType: taskData.taskType, - workflow: workflow - }) - }); - - _setCancellationForAjaxCall(runRequest); - - /* schedule checks */ - var promise = runRequest.then( - function(response) { - taskData.executionId = response.data.executionId; - return _checkStatus(taskData.executionId, CHECK_DELAY); - }, - transformAjaxFailure - ); - - promise.cancel = function timeoutRequest_cancel() { - // calling this method should by itself resolve the promise + /* + * taskData = { + * arguments: { ... }, + * taskType: 'fetchData' or name of R script minus .R, + * phase: 'fetch' | 'preprocess' | 'run', + * } + * + * If it succeeds, the result will be the data returned by the server. + * If it fails, it will return an object with at least these fields: + * { + * status: 0 | , + * statusText: , + * } + */ + service.startScriptExecution = function(taskDataOrig) { + + var taskData = $.extend({}, taskDataOrig); // clone the thing state.currentRequestAbort(); - }; - // no touching necessary when a task is running - window.clearTimeout(state.touchTimeout); - promise.finally(rServeService_scheduleTouch.bind(this)); - - return promise; - }; - - function _setCancellationForAjaxCall(ajax) { - /* request in-flight; aborting is cancelling this request */ - state.currentRequestAbort = function() { ajax.abort(); }; - /* once the request finishes, there's nothing to abort. - * this needs to be the 1st callback, so that later callbacks can - * override this */ - ajax.finally(function() { - state.currentRequestAbort = NOOP_ABORT; - }); - } - - /* set a function to be executed after a period of time and - * simultaneously define the request cancellation function - * as the cancellation of this timeout. - * Return the result as a promise. */ - function _setStatusRequestTimeout(funcToCall, delay /*, ... */) { - var defer = $.Deferred(); - var promise = defer.promise(); - var restOfArguments = Array.prototype.slice.call(arguments) - .splice(2); // arguments after delay - - function _setStatusRequestTimeout_wrappedFunc() { - // we cannot abort by calling clearTimeout() anymore - // funcToCall will probably set its own abort method - state.currentRequestAbort = NOOP_ABORT; - - // funcToCall should itself return a promise or a final result - // but let's cover the case where it throws - var result; - try { - result = funcToCall.apply(undefined, restOfArguments); // promise - result.done(function(x) { defer.resolve(x); }); - result.fail(function(x) { defer.reject(x); }); - } catch (e) { - defer.fail(e); - } - } + var canceler = $q.defer(); + var runRequest = $http({ + url: pageInfo.basePath + '/ScriptExecution/run', + method: 'POST', + timeout: canceler.promise, + responseType: 'json', + data: { + sessionId: state.sessionId, + arguments: taskData.arguments, + taskType: taskData.taskType, + workflow: workflow + } + }); - var timeout = window.setTimeout( - _setStatusRequestTimeout_wrappedFunc, delay); + runRequest.finally(function() { + state.currentRequestAbort = NOOP_ABORT; + }); - state.currentRequestAbort = function() { - clearTimeout(timeout); - if (defer.state() == 'pending') { - defer.reject({ - status: 0, - statusText: 'abort' - }); - } - state.currentRequestAbort = NOOP_ABORT; - }; + state.currentRequestAbort = function() { canceler.resolve(); }; + + /* schedule checks */ + var promise = $q(function(resolve, reject) { + runRequest.then( + function(response) { + taskData.executionId = response.data.executionId; + _checkStatus(taskData.executionId, CHECK_DELAY, resolve, reject); + }, + function(response) { + reject(response.statusText); + } + ); + }); - return promise; - } - - /* aux function of _startScriptExecution. Needs to follow its contract - * with respect to the fail and success result of the promise */ - function _checkStatus(executionId, delay) { - var ajax = $.ajax({ - type: 'GET', - url: pageInfo.basePath + '/ScriptExecution/status', - data: { - sessionId : state.sessionId, - executionId: executionId - } - }); - - state.currentRequestAbort = function() { ajax.abort(); }; - ajax.always(function() { state.currentRequestAbort = NOOP_ABORT; }); - - return ajax.then(function (d) { - if (d.state === 'FINISHED') { - d.executionId = executionId; - return d; - } else if (d.state === 'FAILED') { - return $.Deferred().reject({ - status: 0, - statusText: d.result.exception - }).promise(); - } else { - // else still pending - return _setStatusRequestTimeout( - _checkStatus, delay, executionId, delay); - } - }, transformAjaxFailure); - } + promise.cancel = function() { + // calling this method should by itself resolve the promise + state.currentRequestAbort(); + }; + + // no touching necessary when a task is running + window.clearTimeout(state.touchTimeout); + promise.finally(rServeService_scheduleTouch.bind(this)); - function transformAjaxFailure(response) { - var ret = { - status: response.status, - statusText: response.statusText + return promise; }; - if (response.data !== undefined) { - ret.response = response.data; - } - return ret; - } - - service.downloadJsonFile = function(executionId, filename) { - // Simple GET request example: - return $http({ - method: 'GET', - url: this.urlForFile(executionId, filename) - }); - }; - - - service.urlForFile = function(executionId, filename) { - return pageInfo.basePath + - '/ScriptExecution/downloadFile?sessionId=' + - state.sessionId + - '&executionId=' + - executionId + - '&filename=' + - filename; - }; - - service.loadDataIntoSession = function(conceptKeys, dataConstraints) { - return $q(function(resolve, reject) { - smartRUtils.getSubsetIds().then( - function(subsets) { - var _arg = { - conceptKeys: conceptKeys, - resultInstanceIds: subsets, - projection:'log_intensity' - }; + /* aux function of _startScriptExecution. Needs to follow its contract + * with respect to the fail and success result of the promise */ + function _checkStatus(executionId, delay, resolve, reject) { + var canceler = $q.defer(); + var statusRequest = $http({ + method: 'GET', + url: pageInfo.basePath + '/ScriptExecution/status' + + '?sessionId=' + state.sessionId + + '&executionId=' + executionId + }); - if (typeof dataConstraints !== 'undefined') { - _arg.dataConstraints = dataConstraints; + state.currentRequestAbort = function() { canceler.resolve(); }; + statusRequest.finally(function() { state.currentRequestAbort = NOOP_ABORT; }); + + statusRequest.then( + function (d) { + if (d.data.state === 'FINISHED') { + d.data.executionId = executionId; + resolve(d.data); + } else if (d.data.state === 'FAILED') { + reject(d.data.result.exception); + } else { + // else still pending + window.setTimeout(_checkStatus(executionId, delay, resolve, reject), delay); } - - service.startScriptExecution({ - taskType: 'fetchData', - arguments: _arg - }).then( - function(ret) { resolve('Task complete! State: ' + ret.state); }, - function(ret) { reject(ret.statusText); } - ); }, - function() { - reject('Could not create subsets. Did you select a cohort?'); - } + function(response) { reject(response.statusText); } ); - }); - }; - - service.executeSummaryStats = function(phase) { - return service.startScriptExecution({ - taskType: 'summary', - arguments: { - phase: phase, - projection: 'log_intensity' // always required, even for low-dim data - } - }).then( - function(ret) { - if (ret.result.artifacts.files.length > 0) { - return service.composeSummaryResults(ret.result.artifacts.files, ret.executionId, phase) - .then( - function(result) { return {result: result, msg: 'Task complete! State: ' + ret.state}; }, - transformAjaxFailure + } + + service.downloadJsonFile = function(executionId, filename) { + return $http({ + method: 'GET', + url: this.urlForFile(executionId, filename) + }); + }; + + + service.urlForFile = function(executionId, filename) { + return pageInfo.basePath + + '/ScriptExecution/downloadFile?sessionId=' + state.sessionId + + '&executionId=' + executionId + '&filename=' + filename; + }; + + service.loadDataIntoSession = function(conceptKeys, dataConstraints) { + return $q(function(resolve, reject) { + smartRUtils.getSubsetIds().then( + function(subsets) { + var _arg = { + conceptKeys: conceptKeys, + resultInstanceIds: subsets, + projection:'log_intensity' + }; + + if (typeof dataConstraints !== 'undefined') { + _arg.dataConstraints = dataConstraints; + } + + service.startScriptExecution({ + taskType: 'fetchData', + arguments: _arg + }).then( + resolve, + function(ret) { reject(ret.statusText); } ); - } else { - return {result: {}, msg: 'Task complete! State: ' + ret.state}; - } - }, - transformAjaxFailure - ); - }; + }, + function() { + reject('Could not create subsets!'); + } + ); + }); + }; - service.composeSummaryResults = function(files, executionId, phase) { - return $q(function (resolve, reject) { - var retObj = {summary: [], allSamples: 0}, - fileExt = {fetch: ['.png', 'json'], preprocess:['all.png', 'all.json']}, + service.executeSummaryStats = function(phase) { + return $q(function(resolve, reject) { + service.startScriptExecution({ + taskType: 'summary', + arguments: { + phase: phase, + projection: 'log_intensity' // always required, even for low-dim data + } + }).then( + function(ret) { + if (ret.result.artifacts.files.length > 0) { + service.composeSummaryResults(ret.result.artifacts.files, ret.executionId, phase).then( + function(result) { resolve({result: result}); }, + function(msg) { reject(msg.statusText); } + ); + } else { + resolve({result: {}}); + } + }, + function(msg) { reject(msg.statusText); } + ); + }); + }; + + service.composeSummaryResults = function(files, executionId, phase) { + return $q(function(resolve, reject) { + var retObj = {summary: [], allSamples: 0}, + fileExt = {fetch: ['.png', 'json'], preprocess:['all.png', 'all.json']}, // find matched items in an array by key - _find = function composeSummaryResults_find (key, array) { - // The variable results needs var in this case (without 'var' a global variable is created) - var results = []; - for (var i = 0; i < array.length; i++) { - if (array[i].search(key) > -1) { - results.push(array[i]); + _find = function composeSummaryResults_find (key, array) { + // The variable results needs var in this case (without 'var' a global variable is created) + var results = []; + for (var i = 0; i < array.length; i++) { + if (array[i].search(key) > -1) { + results.push(array[i]); + } } - } - return results; - }, + return results; + }, // process each item - _processItem = function composeSummaryResults_processItem(img, json) { - return $q(function (resolve, reject) { - service.downloadJsonFile(executionId, json).then( - function (d) { - retObj.subsets = d.data.length; - d.data.forEach(function (subset) { - retObj.allSamples += subset.numberOfSamples; - }); - resolve({img: service.urlForFile(executionId, img), json:d}) - }, - function (err) {reject(err);} - ); - }); - }; + _processItem = function composeSummaryResults_processItem(img, json) { + return $q(function(resolve) { + service.downloadJsonFile(executionId, json).then( + function (d) { + retObj.subsets = d.data.length; + d.data.forEach(function (subset) { + retObj.allSamples += subset.numberOfSamples; + }); + resolve({img: service.urlForFile(executionId, img), json:d}); + }, + function (err) { reject(err); } + ); + }); + }; - // first identify image and json files - var _images = _find(fileExt[phase][0], files), - _jsons = _find(fileExt[phase][1], files); + // first identify image and json files + var _images = _find(fileExt[phase][0], files), + _jsons = _find(fileExt[phase][1], files); - // load each json file contents - for (var i = 0; i < _images.length; i++){ - retObj.summary.push(_processItem(_images[i], _jsons[i])); - } + // load each json file contents + for (var i = 0; i < _images.length; i++){ + retObj.summary.push(_processItem(_images[i], _jsons[i])); + } + + $.when.apply($, retObj.summary).then(function () { + resolve(retObj); // when all contents has been loaded + }); + }); + }; - $.when.apply($, retObj.summary).then(function () { - resolve(retObj); // when all contents has been loaded + service.preprocess = function(args) { + return $q(function (resolve, reject) { + service.startScriptExecution({ + taskType: 'preprocess', + arguments: args + }).then( + resolve, + function(ret) { reject(ret.statusText); } + ); }); - }); - }; - - service.preprocess = function(args) { - return $q(function (resolve, reject) { - service.startScriptExecution({ - taskType: 'preprocess', - arguments: args - }).then( - function(ret) { resolve('Task complete! State: ' + ret.state); }, - function(ret) { reject(ret.response); } - ); - }); - }; + }; - return service; -}]); + return service; + }]); diff --git a/web-app/js/smartR/_angular/services/smartRUtils.js b/web-app/js/smartR/_angular/services/smartRUtils.js index cb3a8f4..0a45e73 100644 --- a/web-app/js/smartR/_angular/services/smartRUtils.js +++ b/web-app/js/smartR/_angular/services/smartRUtils.js @@ -1,5 +1,7 @@ //# sourceURL=smartRUtils.js +'use strict'; + window.smartRApp.factory('smartRUtils', ['$q', function($q) { var service = {}; @@ -25,8 +27,12 @@ window.smartRApp.factory('smartRUtils', ['$q', function($q) { service.makeSafeForCSS = function smartRUtils_makeSafeForCSS(str) { return String(str).replace(/[^a-z0-9]/g, function(s) { var c = s.charCodeAt(0); - if (c == 32) return '-'; - if (c >= 65 && c <= 90) return '_' + s.toLowerCase(); + if (c === 32) { + return '-'; + } + if (c >= 65 && c <= 90) { + return '_' + s.toLowerCase(); + } return '__' + ('000' + c.toString(16)).slice(-4); }); }; @@ -45,14 +51,12 @@ window.smartRApp.factory('smartRUtils', ['$q', function($q) { service.mouseX = function(root) { var svg = $(root).children('svg'); - var smartRPanel = $('#smartRPanel'); var mouseXPos = typeof d3.event.sourceEvent !== 'undefined' ? d3.event.sourceEvent.pageX : d3.event.clientX; return mouseXPos - svg.offset().left + svg.position().left; }; service.mouseY = function(root) { var svg = $(root).children('svg'); - var smartRPanel = $('#smartRPanel'); var mouseYPos = typeof d3.event.sourceEvent !== 'undefined' ? d3.event.sourceEvent.pageY : d3.event.clientY; return mouseYPos - svg.offset().top + svg.position().top; }; From 2e5806981807c3bd989cf89c6469436ea1daa273 Mon Sep 17 00:00:00 2001 From: Sascha Herzinger Date: Thu, 21 Apr 2016 09:23:09 +0200 Subject: [PATCH 2/6] fixed status scheduler --- .../js/smartR/_angular/services/rServeService.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/web-app/js/smartR/_angular/services/rServeService.js b/web-app/js/smartR/_angular/services/rServeService.js index 8376ee6..365d366 100644 --- a/web-app/js/smartR/_angular/services/rServeService.js +++ b/web-app/js/smartR/_angular/services/rServeService.js @@ -12,7 +12,7 @@ window.smartRApp.factory('rServeService', [ var NOOP_ABORT = function() {}; var TIMEOUT = 10000 /* 10 s */; - var CHECK_DELAY = 1000; + var CHECK_DELAY = 500; var SESSION_TOUCH_DELAY = 9 * 60 * 1000; /* 9 min; session timeout is 10 */ /* we only support one session at a time */ @@ -169,7 +169,7 @@ window.smartRApp.factory('rServeService', [ runRequest.then( function(response) { taskData.executionId = response.data.executionId; - _checkStatus(taskData.executionId, CHECK_DELAY, resolve, reject); + _checkStatus(taskData.executionId, resolve, reject); }, function(response) { reject(response.statusText); @@ -191,17 +191,18 @@ window.smartRApp.factory('rServeService', [ /* aux function of _startScriptExecution. Needs to follow its contract * with respect to the fail and success result of the promise */ - function _checkStatus(executionId, delay, resolve, reject) { + function _checkStatus(executionId, resolve, reject) { var canceler = $q.defer(); var statusRequest = $http({ method: 'GET', + timeout: canceler.promise, url: pageInfo.basePath + '/ScriptExecution/status' + '?sessionId=' + state.sessionId + '&executionId=' + executionId }); - state.currentRequestAbort = function() { canceler.resolve(); }; statusRequest.finally(function() { state.currentRequestAbort = NOOP_ABORT; }); + state.currentRequestAbort = function() { canceler.resolve(); }; statusRequest.then( function (d) { @@ -212,7 +213,9 @@ window.smartRApp.factory('rServeService', [ reject(d.data.result.exception); } else { // else still pending - window.setTimeout(_checkStatus(executionId, delay, resolve, reject), delay); + window.setTimeout(function() { + _checkStatus(executionId, resolve, reject); + }, CHECK_DELAY); } }, function(response) { reject(response.statusText); } From d1f587db033d32dce6d98e64e19377f6352c0dd4 Mon Sep 17 00:00:00 2001 From: Sascha Herzinger Date: Thu, 21 Apr 2016 09:24:53 +0200 Subject: [PATCH 3/6] make tests green again --- web-app/js/smartR/_angular/directives/fetchButton.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web-app/js/smartR/_angular/directives/fetchButton.js b/web-app/js/smartR/_angular/directives/fetchButton.js index 7beb031..707a33b 100644 --- a/web-app/js/smartR/_angular/directives/fetchButton.js +++ b/web-app/js/smartR/_angular/directives/fetchButton.js @@ -85,13 +85,13 @@ window.smartRApp.directive('fetchButton', [ template_msg.innerHTML = 'Fetching data, please wait _'; if (smartRUtils.countCohorts() === 0) { - _onFailure('Error: No cohorts selected!'); + _onFailure('No cohorts selected!'); return; } var conceptKeys = smartRUtils.conceptBoxMapToConceptKeys(scope.conceptMap); if ($.isEmptyObject(conceptKeys)) { - _onFailure('Error: No concepts selected!'); + _onFailure('No concepts selected!'); return; } From cd5c6352e4b0529bec88b2b19ae18f749e43682b Mon Sep 17 00:00:00 2001 From: Sascha Herzinger Date: Thu, 21 Apr 2016 09:34:24 +0200 Subject: [PATCH 4/6] :fish: --- web-app/js/smartR/_angular/services/rServeService.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/web-app/js/smartR/_angular/services/rServeService.js b/web-app/js/smartR/_angular/services/rServeService.js index 365d366..a234ea8 100644 --- a/web-app/js/smartR/_angular/services/rServeService.js +++ b/web-app/js/smartR/_angular/services/rServeService.js @@ -11,9 +11,9 @@ window.smartRApp.factory('rServeService', [ var service = {}; var NOOP_ABORT = function() {}; - var TIMEOUT = 10000 /* 10 s */; - var CHECK_DELAY = 500; - var SESSION_TOUCH_DELAY = 9 * 60 * 1000; /* 9 min; session timeout is 10 */ + var TIMEOUT = 10000; // 10 s + var CHECK_DELAY = 500; // 0.5 s + var SESSION_TOUCH_DELAY = 60000; // 1 min /* we only support one session at a time */ @@ -79,10 +79,9 @@ window.smartRApp.factory('rServeService', [ }; function rServeService_scheduleTouch() { - var sessionId = state.sessionId; window.clearTimeout(state.touchTimeout); state.touchTimeout = window.setTimeout(function() { - service.touch(sessionId); + service.touch(state.sessionId); }, SESSION_TOUCH_DELAY); } From bbef5ae2ff30b18bb85070027a5487ab3b9b01e3 Mon Sep 17 00:00:00 2001 From: Sascha Herzinger Date: Thu, 21 Apr 2016 09:38:25 +0200 Subject: [PATCH 5/6] another :fish: --- web-app/js/smartR/_angular/services/rServeService.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/web-app/js/smartR/_angular/services/rServeService.js b/web-app/js/smartR/_angular/services/rServeService.js index a234ea8..deb4888 100644 --- a/web-app/js/smartR/_angular/services/rServeService.js +++ b/web-app/js/smartR/_angular/services/rServeService.js @@ -130,13 +130,6 @@ window.smartRApp.factory('rServeService', [ * taskType: 'fetchData' or name of R script minus .R, * phase: 'fetch' | 'preprocess' | 'run', * } - * - * If it succeeds, the result will be the data returned by the server. - * If it fails, it will return an object with at least these fields: - * { - * status: 0 | , - * statusText: , - * } */ service.startScriptExecution = function(taskDataOrig) { @@ -289,6 +282,7 @@ window.smartRApp.factory('rServeService', [ }; service.composeSummaryResults = function(files, executionId, phase) { + // FIXME: errors from downloadJsonFile do not lead to a reject return $q(function(resolve, reject) { var retObj = {summary: [], allSamples: 0}, fileExt = {fetch: ['.png', 'json'], preprocess:['all.png', 'all.json']}, From b378807fb25d6339fed2dfad247528571be7a380 Mon Sep 17 00:00:00 2001 From: Sascha Herzinger Date: Thu, 21 Apr 2016 14:23:08 +0200 Subject: [PATCH 6/6] SH-146 and some code refactoring --- .jshintrc | 3 +- grails-app/conf/SmartRResources.groovy | 1 + grails-app/views/layouts/_boxplot.gsp | 18 +- web-app/HeimScripts/boxplot/run.R | 72 +- .../js/smartR/_angular/controllers/boxplot.js | 12 +- .../smartR/_angular/directives/runButton.js | 1 - .../_angular/services/controlElements.js | 373 +++ .../smartR/_angular/services/smartRUtils.js | 14 +- web-app/js/smartR/_angular/viz/d3Boxplot.js | 35 +- .../js/smartR/_angular/viz/d3Correlation.js | 809 ++--- web-app/js/smartR/_angular/viz/d3Heatmap.js | 2707 +++++++++-------- web-app/js/smartR/smartR.js | 370 +-- 12 files changed, 2224 insertions(+), 2191 deletions(-) create mode 100644 web-app/js/smartR/_angular/services/controlElements.js diff --git a/.jshintrc b/.jshintrc index 0bf30b8..e36808b 100644 --- a/.jshintrc +++ b/.jshintrc @@ -90,6 +90,7 @@ "module": true, "inject": true, "pageInfo": true, - "d3": true + "d3": true, + "Ext": true } // additional predefined global variables } diff --git a/grails-app/conf/SmartRResources.groovy b/grails-app/conf/SmartRResources.groovy index 3df7209..d932d93 100644 --- a/grails-app/conf/SmartRResources.groovy +++ b/grails-app/conf/SmartRResources.groovy @@ -33,6 +33,7 @@ modules = { resource url: [plugin: 'smart-r', dir: 'js/smartR/_angular/services', file: 'rServeService.js'] resource url: [plugin: 'smart-r', dir: 'js/smartR/_angular/services', file: 'smartRUtils.js'] resource url: [plugin: 'smart-r', dir: 'js/smartR/_angular/services', file: 'commonWorkflowService.js'] + resource url: [plugin: 'smart-r', dir: 'js/smartR/_angular/services', file: 'controlElements.js'] } smartR_all { diff --git a/grails-app/views/layouts/_boxplot.gsp b/grails-app/views/layouts/_boxplot.gsp index 39e0fa2..9d2b110 100644 --- a/grails-app/views/layouts/_boxplot.gsp +++ b/grails-app/views/layouts/_boxplot.gsp @@ -14,14 +14,16 @@ label="Numerical Variable" tooltip="Select a single numerical variable that you would like to have displayed."> - - + %{--Nice idea but somehow lost it's initial purpose because cross-study support is gone. + Maybe implement later--}% + %{----}% + %{----}%

diff --git a/web-app/HeimScripts/boxplot/run.R b/web-app/HeimScripts/boxplot/run.R index e5499cf..565442c 100644 --- a/web-app/HeimScripts/boxplot/run.R +++ b/web-app/HeimScripts/boxplot/run.R @@ -1,47 +1,45 @@ -main <- function( excludedPatientIDs = integer() ) { - datapoints <- parse.input(sourceLabel="datapoints", loaded_variables=loaded_variables, type="numerical") - datapoints <- na.omit(datapoints) - colnames(datapoints)[2] <- 'value' - - subsets <- parse.input(sourceLabel="subsets", loaded_variables=loaded_variables, type="categorical") - - if (nrow(subsets) > 0) { - df <- merge(datapoints, subsets, by="patientID") - levels(df$category) <- c(levels(df$category), "no subset") - df$category[df$category == ""] <- "no subset" - } else { - df <- datapoints - df$category <- "no subset" - } - - df$jitter <- runif(nrow(df), -0.5, 0.5) - - if (! is.null(excludedPatientIDs)) { - df <- df[! df$patientID %in% excludedPatientIDs, ] - } - patientIDs <- df$patientID +main <- function(excludedPatientIDs = integer()) { output <- list() output$concept <- fetch_params$ontologyTerms$datapoints_n0$fullName - output$globalMin <- min(df[,2]) - output$globalMax <- max(df[,2]) - output$categories <- unique(df$category) output$excludedPatientIDs <- excludedPatientIDs - for (cat in unique(df$category)) { - subset <- df[df$category == cat,] - bxp <- boxplot(subset[,2], plot=FALSE) - output[[cat]] <- list() - output[[cat]]$lowerWhisker <- bxp$stats[1] - output[[cat]]$lowerHinge <- bxp$stats[2] - output[[cat]]$median <- bxp$stats[3] - output[[cat]]$upperHinge <- bxp$stats[4] - output[[cat]]$upperWhisker <- bxp$stats[5] - outlier <- subset[,2] > output[[cat]]$upperWhisker | subset[,2] < output[[cat]]$lowerWhisker - subset$outlier <- outlier - output[[cat]]$points <- subset + df1 <- loaded_variables$datapoints_n0_s1 + df1 <- prepareData(df1, excludedPatientIDs) + output <- addBoxplotStats(output, "Subset 1", df1) + output$globalMin <- min(df1$value) + output$globalMax <- max(df1$value) + + if(!is.null(loaded_variables$datapoints_n0_s2)) { + df2 <- loaded_variables$datapoints_n0_s2 + df2 <- prepareData(df2, excludedPatientIDs) + output <- addBoxplotStats(output, "Subset 2", df2) + output$globalMin <- min(df1$value, df2$value) + output$globalMax <- max(df1$value, df2$value) } toJSON(output) } + +prepareData <- function(df, excludedPatientIDs) { + df <- na.omit(df) + df$jitter <- runif(nrow(df), -0.5, 0.5) + colnames(df) <- c("patientID", "value", "jitter") + df <- df[!df$patientID %in% excludedPatientIDs, ] + df +} + +addBoxplotStats <- function(output, subset, df) { + bxp <- boxplot(df$value, plot=FALSE) + output[[subset]] <- list() + output[[subset]]$lowerWhisker <- bxp$stats[1] + output[[subset]]$lowerHinge <- bxp$stats[2] + output[[subset]]$median <- bxp$stats[3] + output[[subset]]$upperHinge <- bxp$stats[4] + output[[subset]]$upperWhisker <- bxp$stats[5] + outlier <- df$value > output[[subset]]$upperWhisker | df$value < output[[subset]]$lowerWhisker + df$outlier <- outlier + output[[subset]]$rawData <- df + output +} diff --git a/web-app/js/smartR/_angular/controllers/boxplot.js b/web-app/js/smartR/_angular/controllers/boxplot.js index a6a44bd..608646d 100644 --- a/web-app/js/smartR/_angular/controllers/boxplot.js +++ b/web-app/js/smartR/_angular/controllers/boxplot.js @@ -1,13 +1,17 @@ -window.smartRApp.controller('BoxplotController', - ['$scope', 'smartRUtils', 'commonWorkflowService', function($scope, smartRUtils, commonWorkflowService) { +'use strict'; + +window.smartRApp.controller('BoxplotController', [ + '$scope', + 'smartRUtils', + 'commonWorkflowService', + function($scope, smartRUtils, commonWorkflowService) { commonWorkflowService.initializeWorkflow('boxplot', $scope); // model $scope.conceptBoxes = { - datapoints: [], - subsets: [] + datapoints: [] }; $scope.scriptResults = {}; $scope.params = {}; diff --git a/web-app/js/smartR/_angular/directives/runButton.js b/web-app/js/smartR/_angular/directives/runButton.js index 9d47472..a3e83b3 100644 --- a/web-app/js/smartR/_angular/directives/runButton.js +++ b/web-app/js/smartR/_angular/directives/runButton.js @@ -38,7 +38,6 @@ window.smartRApp.directive('runButton', [ var _prepareResults = function(response) { if (scope.serialized) { - console.log('hi') // when results are serialized, we need to deserialized them by // downloading the results files. rServeService.downloadJsonFile(response.executionId, 'heatmap.json').then( diff --git a/web-app/js/smartR/_angular/services/controlElements.js b/web-app/js/smartR/_angular/services/controlElements.js new file mode 100644 index 0000000..7a3207f --- /dev/null +++ b/web-app/js/smartR/_angular/services/controlElements.js @@ -0,0 +1,373 @@ +//# sourceURL=controlElements.js + +'use strict'; + +window.smartRApp.factory('controlElements', ['$q', function($q) { + + var service = {}; + + service.createD3Button = function(args) { + var button = args.location.append('g'); + + var box = button.append('rect') + .attr('x', args.x) + .attr('y', args.y) + .attr('rx', 3) + .attr('ry', 3) + .attr('width', args.width) + .attr('height', args.height) + .style('stroke-width', '1px') + .style('stroke', '#009ac9') + .style('fill', '#009ac9') + .style('cursor', 'pointer') + .on('mouseover', function() { + box.transition() + .duration(300) + .style('fill', '#ffffff'); + text.transition() + .duration(300) + .style('fill', '#009ac9'); + }) + .on('mouseout', function() { + box.transition() + .duration(300) + .style('fill', '#009ac9'); + text.transition() + .duration(300) + .style('fill', '#ffffff'); + }) + .on('click', function() { return args.callback(); }); + + var text = button.append('text') + .attr('x', args.x + args.width / 2) + .attr('y', args.y + args.height / 2) + .attr('dy', '0.35em') + .style('pointer-events', 'none') + .style('text-anchor', 'middle') + .style('fill', '#ffffff') + .style('font-size', '14px') + .text(args.label) + + return button + } + + service.createD3Switch = function(args) { + var switcher = args.location.append('g') + + var checked = args.checked + var color = checked ? 'green' : 'red' + + var box = switcher.append('rect') + .attr('x', args.x) + .attr('y', args.y) + .attr('rx', 3) + .attr('ry', 3) + .attr('width', args.width) + .attr('height', args.height) + .style('stroke-width', '1px') + .style('stroke', color) + .style('fill', color) + .style('cursor', 'pointer') + .on('click', function() { + if (color === 'green') { + box.transition() + .duration(300) + .style('stroke', 'red') + .style('fill', 'red') + color = 'red' + checked = false + } else { + box.transition() + .duration(300) + .style('stroke', 'green') + .style('fill', 'green') + color = 'green' + checked = true + } + text.text(checked ? args.onlabel : args.offlabel) + args.callback(checked) + }) + + var text = switcher.append('text') + .attr('x', args.x + args.width / 2) + .attr('y', args.y + args.height / 2) + .attr('dy', '0.35em') + .style('pointer-events', 'none') + .style('text-anchor', 'middle') + .style('fill', '#ffffff') + .style('font-size', '14px') + .text(checked ? args.onlabel : args.offlabel) + + return switcher + } + + service.createD3Dropdown = function(args) { + function shrink() { + dropdown.selectAll('.itemBox') + .attr('y', args.y + args.height) + .style('visibility', 'hidden') + dropdown.selectAll('.itemText') + .attr('y', args.y + args.height + args.height / 2) + .style('visibility', 'hidden') + itemHovered = false + hovered = false + itemHovered = false + } + var dropdown = args.location.append('g') + + var hovered = false + var itemHovered = false + + var itemBox = dropdown.selectAll('.itemBox') + .data(args.items, function(item) { return item.label }) + + itemBox.enter() + .append('rect') + .attr('class', 'itemBox') + .attr('x', args.x) + .attr('y', args.y + args.height) + .attr('rx', 0) + .attr('ry', 0) + .attr('width', args.width) + .attr('height', args.height) + .style('cursor', 'pointer') + .style('stroke-width', '2px') + .style('stroke', '#ffffff') + .style('fill', '#E3E3E3') + .style('visibility', 'hidden') + .on('mouseover', function() { + itemHovered = true + d3.select(this) + .style('fill', '#009ac9') + }) + .on('mouseout', function() { + itemHovered = false + d3.select(this) + .style('fill', '#E3E3E3') + setTimeout(function() { + if (! hovered && ! itemHovered) { + shrink() + } + }, 50) + }) + .on('click', function(d) { d.callback() }) + + var itemText = dropdown.selectAll('.itemText') + .data(args.items, function(item) { return item.label }) + + itemText + .enter() + .append('text') + .attr('class', 'itemText') + .attr('x', args.x + args.width / 2) + .attr('y', args.y + args.height + args.height / 2) + .attr('dy', '0.35em') + .style('pointer-events', 'none') + .style('text-anchor', 'middle') + .style('fill', '#000000') + .style('font-size', '14px') + .style('visibility', 'hidden') + .text(function(d) { return d.label }) + + var box = dropdown.append('rect') + .attr('x', args.x) + .attr('y', args.y) + .attr('rx', 3) + .attr('ry', 3) + .attr('width', args.width) + .attr('height', args.height) + .style('stroke-width', '1px') + .style('stroke', '#009ac9') + .style('fill', '#009ac9') + .on('mouseover', function() { + if (hovered) { + return + } + dropdown.selectAll('.itemBox') + .transition() + .duration(300) + .style('visibility', 'visible') + .attr('y', function(d) { + var labels = args.items.map(function(item) { return item.label; }); + var idx = labels.indexOf(d.label); + return 2 + args.y + (idx + 1) * args.height + }) + + dropdown.selectAll('.itemText') + .transition() + .duration(300) + .style('visibility', 'visible') + .attr('y', function(d) { + var labels = args.items.map(function(item) { return item.label; }); + var idx = labels.indexOf(d.label); + return 2 + args.y + (idx + 1) * args.height + args.height / 2 + }) + + hovered = true + }) + .on('mouseout', function() { + hovered = false + setTimeout(function() { + if (! hovered && ! itemHovered) { + shrink() + } + }, 50) + setTimeout(function() { // first check is not enough if animation interrupts it + if (! hovered && ! itemHovered) { + shrink() + } + }, 350) + }) + + var text = dropdown.append('text') + .attr('class', 'buttonText') + .attr('x', args.x + args.width / 2) + .attr('y', args.y + args.height / 2) + .attr('dy', '0.35em') + .style('pointer-events', 'none') + .style('text-anchor', 'middle') + .style('fill', '#ffffff') + .style('font-size', '14px') + .text(args.label) + + return dropdown + } + + service.createD3Slider = function(args) { + var slider = args.location.append('g') + + var lineGen = d3.svg.line() + .x(function(d) { return d.x }) + .y(function(d) { return d.y }) + .interpolate('linear') + + var lineData = [ + {x: args.x, y: args.y + args.height}, + {x: args.x, y: args.y + 0.75 * args.height}, + {x: args.x + args.width, y: args.y + 0.75 * args.height}, + {x: args.x + args.width, y: args.y + args.height} + ] + + var sliderScale = d3.scale.linear() + .domain([args.min, args.max]) + .range([args.x, args.x + args.width]) + + slider.append('path') + .attr('d', lineGen(lineData)) + .style('pointer-events', 'none') + .style('stroke', '#009ac9') + .style('stroke-width', '2px') + .style('shape-rendering', 'crispEdges') + .style('fill', 'none') + + slider.append('text') + .attr('x', args.x) + .attr('y', args.y + args.height + 10) + .attr('dy', '0.35em') + .style('pointer-events', 'none') + .style('text-anchor', 'middle') + .style('fill', '#000000') + .style('font-size', '9px') + .text(args.min) + + slider.append('text') + .attr('x', args.x + args.width) + .attr('y', args.y + args.height + 10) + .attr('dy', '0.35em') + .style('pointer-events', 'none') + .style('text-anchor', 'middle') + .style('fill', '#000000') + .style('font-size', '9px') + .text(args.max) + + slider.append('text') + .attr('x', args.x + args.width / 2) + .attr('y', args.y + args.height) + .attr('dy', '0.35em') + .style('pointer-events', 'none') + .style('text-anchor', 'middle') + .style('fill', '#000000') + .style('font-size', '14px') + .text(args.label) + + var currentValue = args.init + + function move() { + var xPos = d3.event.x + if (xPos < args.x) { + xPos = args.x + } else if (xPos > args.x + args.width) { + xPos = args.x + args.width + } + + currentValue = Number(sliderScale.invert(xPos)).toFixed(5) + + dragger + .attr('x', xPos - 20) + handle + .attr('cx', xPos) + pointer + .attr('x1', xPos) + .attr('x2', xPos) + value + .attr('x', xPos + 10) + .text(currentValue) + } + + var drag = d3.behavior.drag() + .on('drag', move) + .on(args.trigger, function() { args.callback(currentValue) }) + + var dragger = slider.append('rect') + .attr('x', sliderScale(args.init) - 20) + .attr('y', args.y) + .attr('width', 40) + .attr('height', args.height) + .style('opacity', 0) + .style('cursor', 'pointer') + .on('mouseover', function() { + handle + .style('fill', '#009ac9') + pointer + .style('stroke', '#009ac9') + }) + .on('mouseout', function() { + handle + .style('fill', '#000000') + pointer + .style('stroke', '#000000') + }) + .call(drag) + + var handle = slider.append('circle') + .attr('cx', sliderScale(args.init)) + .attr('cy', args.y + 10) + .attr('r', 6) + .style('pointer-events', 'none') + .style('fill', '#000000') + + var pointer = slider.append('line') + .attr('x1', sliderScale(args.init)) + .attr('y1', args.y + 10) + .attr('x2', sliderScale(args.init)) + .attr('y2', args.y + 0.75 * args.height) + .style('pointer-events', 'none') + .style('stroke', '#000000') + .style('stroke-width', '1px') + + var value = slider.append('text') + .attr('x', sliderScale(args.init) + 10) + .attr('y', args.y + 10) + .attr('dy', '0.35em') + .style('pointer-events', 'none') + .style('text-anchor', 'start') + .style('fill', '#000000') + .style('font-size', '10px') + .text(args.init) + + return slider + } + + return service; + +}]); \ No newline at end of file diff --git a/web-app/js/smartR/_angular/services/smartRUtils.js b/web-app/js/smartR/_angular/services/smartRUtils.js index 0a45e73..5066c90 100644 --- a/web-app/js/smartR/_angular/services/smartRUtils.js +++ b/web-app/js/smartR/_angular/services/smartRUtils.js @@ -42,13 +42,7 @@ window.smartRApp.factory('smartRUtils', ['$q', function($q) { split = split.filter(function(str) { return str !== ''; }); return split[split.length - 2] + '/' + split[split.length - 1]; }; - - d3.selection.prototype.moveToFront = function () { - return this.forEach(function () { - this.parentNode.appendChild(this); - }); - }; - + service.mouseX = function(root) { var svg = $(root).children('svg'); var mouseXPos = typeof d3.event.sourceEvent !== 'undefined' ? d3.event.sourceEvent.pageX : d3.event.clientX; @@ -60,6 +54,12 @@ window.smartRApp.factory('smartRUtils', ['$q', function($q) { var mouseYPos = typeof d3.event.sourceEvent !== 'undefined' ? d3.event.sourceEvent.pageY : d3.event.clientY; return mouseYPos - svg.offset().top + svg.position().top; }; + + service.getMaxWidth = function(selection) { + return selection[0].map(function (d) { + return d.getBBox().width; + }).max(); + }; service.countCohorts = function() { return !window.isSubsetEmpty(1) + !window.isSubsetEmpty(2); diff --git a/web-app/js/smartR/_angular/viz/d3Boxplot.js b/web-app/js/smartR/_angular/viz/d3Boxplot.js index f345599..32f60fe 100644 --- a/web-app/js/smartR/_angular/viz/d3Boxplot.js +++ b/web-app/js/smartR/_angular/viz/d3Boxplot.js @@ -2,7 +2,11 @@ 'use strict'; -window.smartRApp.directive('boxplot', ['smartRUtils', 'rServeService', function(smartRUtils, rServeService) { +window.smartRApp.directive('boxplot', [ + 'smartRUtils', + 'rServeService', + 'controlElements', + function(smartRUtils, rServeService, controlElements) { return { restrict: 'E', @@ -27,15 +31,15 @@ window.smartRApp.directive('boxplot', ['smartRUtils', 'rServeService', function( function createBoxplot(scope, root) { var concept = '', - globalMin = 0, - globalMax = 0, + globalMin = Number.MIN_VALUE, + globalMax = Number.MAX_VALUE, categories = [], excludedPatientIDs = []; function setData(data) { concept = data.concept[0]; globalMin = data.globalMin[0]; globalMax = data.globalMax[0]; - categories = data.categories.sort(); + categories = data['Subset 2'] ? ['Subset 1', 'Subset 2'] : ['Subset 1']; excludedPatientIDs = data.excludedPatientIDs; } setData(scope.data); @@ -193,11 +197,11 @@ window.smartRApp.directive('boxplot', ['smartRUtils', 'rServeService', function( }; } - function gaussKernel(scale) { - return function (u) { - return Math.exp(-u * u / 2) / Math.sqrt(2 * Math.PI) / scale; - }; - } + // function gaussKernel(scale) { + // return function (u) { + // return Math.exp(-u * u / 2) / Math.sqrt(2 * Math.PI) / scale; + // }; + // } function swapKDE(checked) { if (!checked) { @@ -233,7 +237,6 @@ window.smartRApp.directive('boxplot', ['smartRUtils', 'rServeService', function( var params = scope.data[category]; createBox(params, category, boxes[category]); }); - d3.selectAll('.text, .line, .point').moveToFront(); function createBox(params, category, box) { var boxLabel = shortenNodeLabel(category); @@ -368,7 +371,7 @@ window.smartRApp.directive('boxplot', ['smartRUtils', 'rServeService', function( .text(params.median); var point = box.selectAll('.point') - .data(params.points, function (d) { return boxLabel + '-' + d.patientID; }); + .data(params.rawData, function (d) { return boxLabel + '-' + d.patientID; }); point.enter() .append('circle') @@ -409,7 +412,7 @@ window.smartRApp.directive('boxplot', ['smartRUtils', 'rServeService', function( var yCopy = y.copy(); yCopy.domain([params.lowerWhisker, params.upperWhisker]); var kde = kernelDensityEstimator(epanechnikovKernel(6), yCopy.ticks(100)); - var values = params.points.map(function (d) { return d.value; }); + var values = params.rawData.map(function (d) { return d.value; }); var estFun = kde(values); var kdeDomain = d3.extent(estFun, function (d) { return d[1]; }); var kdeScale = d3.scale.linear() @@ -443,7 +446,7 @@ window.smartRApp.directive('boxplot', ['smartRUtils', 'rServeService', function( var buttonWidth = 200; var buttonHeight = 40; var padding = 5; - createD3Button({ + controlElements.createD3Button({ location: boxplot, label: 'Remove Outliers', x: -280, @@ -452,7 +455,7 @@ window.smartRApp.directive('boxplot', ['smartRUtils', 'rServeService', function( height: buttonHeight, callback: removeOutliers }); - createD3Button({ + controlElements.createD3Button({ location: boxplot, label: 'Reset', x: -280, @@ -462,7 +465,7 @@ window.smartRApp.directive('boxplot', ['smartRUtils', 'rServeService', function( callback: reset }); - createD3Switch({ + controlElements.createD3Switch({ location: boxplot, onlabel: 'Density Estimation ON', offlabel: 'Density Estimation OFF', @@ -473,7 +476,7 @@ window.smartRApp.directive('boxplot', ['smartRUtils', 'rServeService', function( callback: swapKDE, checked: false }); - createD3Switch({ + controlElements.createD3Switch({ location: boxplot, onlabel: 'Jitter Datapoints ON', offlabel: 'Jitter Datapoints OFF', diff --git a/web-app/js/smartR/_angular/viz/d3Correlation.js b/web-app/js/smartR/_angular/viz/d3Correlation.js index 3358fb8..4e9d5cb 100644 --- a/web-app/js/smartR/_angular/viz/d3Correlation.js +++ b/web-app/js/smartR/_angular/viz/d3Correlation.js @@ -1,428 +1,437 @@ //# sourceURL=d3Correlation.js -window.smartRApp.directive('correlationPlot', ['smartRUtils', 'rServeService', function(smartRUtils, rServeService) { - - return { - restrict: 'E', - scope: { - data: '=', - width: '@', - height: '@' - }, - link: function (scope, element) { - - /** - * Watch data model (which is only changed by ajax calls when we want to (re)draw everything) - */ - scope.$watch('data', function() { - $(element[0]).empty(); - if (! $.isEmptyObject(scope.data)) { - createCorrelationViz(scope, element[0]); - } - }); - } - }; - - function createCorrelationViz(scope, root) { - var animationDuration = 500; - var bins = 10; - var w = scope.width; - var h = scope.height; - var margin = {top: 20, right: 20, bottom: h / 4, left: w / 4}; - var width = w * 3 / 4 - margin.left - margin.right; - var height = h * 3 / 4 - margin.top - margin.bottom; - var bottomHistHeight = margin.bottom; - var leftHistHeight = margin.left; - var colors = ['#33FF33', '#3399FF', '#CC9900', '#CC99FF', '#FFFF00', 'blue']; - var x = d3.scale.linear() - .domain(d3.extent(scope.data.points, function(d) { return d.x; })) - .range([0, width]); - var y = d3.scale.linear() - .domain(d3.extent(scope.data.points, function(d) { return d.y; })) - .range([height, 0]); - - var annotations = scope.data.annotations.sort(); - var xArrLabel = scope.data.xArrLabel[0]; - var yArrLabel = scope.data.yArrLabel[0]; - - var correlation, - pvalue, - regLineSlope, - regLineYIntercept, - patientIDs, - points, - method, - minX, - maxX, - minY, - maxY; - function setData(data) { - correlation = data.correlation[0]; - pvalue = data.pvalue[0]; - regLineSlope = data.regLineSlope[0]; - regLineYIntercept = data.regLineYIntercept[0]; - method = data.method[0]; - patientIDs = data.patientIDs; - points = data.points; - minX = data.points.min(function(d) { return d.x }); - maxX = data.points.max(function(d) { return d.x }); - minY = data.points.min(function(d) { return d.y }); - maxY = data.points.max(function(d) { return d.y }); - } - - setData(scope.data); - - function updateStatistics(patientIDs, scatterUpdate, init) { - scatterUpdate = scatterUpdate === undefined ? false : scatterUpdate; - init = init === undefined ? false : init; - var arguments = { selectedPatientIDs: patientIDs }; - - rServeService.startScriptExecution({ - taskType: 'run', - arguments: arguments - }).then( - function (response) { - var results = JSON.parse(response.result.artifacts.value); - if (init) { - scope.data = results; - } else { - setData(results); - if (scatterUpdate) updateScatterplot(); - updateRegressionLine(); - updateLegend(); - updateHistogram(); +'use strict'; + +window.smartRApp.directive('correlationPlot', [ + 'smartRUtils', + 'rServeService', + function(smartRUtils, rServeService) { + + return { + restrict: 'E', + scope: { + data: '=', + width: '@', + height: '@' + }, + link: function (scope, element) { + + /** + * Watch data model (which is only changed by ajax calls when we want to (re)draw everything) + */ + scope.$watch('data', function() { + $(element[0]).empty(); + if (! $.isEmptyObject(scope.data)) { + createCorrelationViz(scope, element[0]); } - }, - function (response) { - console.error(' Failure: ' + response.statusText); - } - ); - } - - var svg = d3.select(root).append('svg') - .attr('width', width + margin.left + margin.right) - .attr('height', height + margin.top + margin.bottom) - .append('g') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') - .on('contextmenu', function() { - d3.event.preventDefault(); - contextMenu - .style('visibility', 'visible') - .style('left', smartRUtils.mouseX(root) + 'px') - .style('top', smartRUtils.mouseY(root) + 'px') - }); + }); + } + }; + + function createCorrelationViz(scope, root) { + var animationDuration = 500; + var bins = 10; + var w = scope.width; + var h = scope.height; + var margin = {top: 20, right: 20, bottom: h / 4, left: w / 4}; + var width = w * 3 / 4 - margin.left - margin.right; + var height = h * 3 / 4 - margin.top - margin.bottom; + var bottomHistHeight = margin.bottom; + var leftHistHeight = margin.left; + var colors = ['#33FF33', '#3399FF', '#CC9900', '#CC99FF', '#FFFF00', 'blue']; + var x = d3.scale.linear() + .domain(d3.extent(scope.data.points, function(d) { return d.x; })) + .range([0, width]); + var y = d3.scale.linear() + .domain(d3.extent(scope.data.points, function(d) { return d.y; })) + .range([height, 0]); + + var annotations = scope.data.annotations.sort(); + var xArrLabel = scope.data.xArrLabel[0]; + var yArrLabel = scope.data.yArrLabel[0]; + + var correlation, + pvalue, + regLineSlope, + regLineYIntercept, + patientIDs, + points, + method, + minX, + maxX, + minY, + maxY; + function setData(data) { + correlation = data.correlation[0]; + pvalue = data.pvalue[0]; + regLineSlope = data.regLineSlope[0]; + regLineYIntercept = data.regLineYIntercept[0]; + method = data.method[0]; + patientIDs = data.patientIDs; + points = data.points; + minX = data.points.min(function(d) { return d.x; }); + maxX = data.points.max(function(d) { return d.x; }); + minY = data.points.min(function(d) { return d.y; }); + maxY = data.points.max(function(d) { return d.y; }); + } - var tooltip = d3.select(root).append('div') - .attr('class', 'tooltip') - .style('visibility', 'hidden'); + setData(scope.data); + + function updateStatistics(patientIDs, scatterUpdate, init) { + scatterUpdate = scatterUpdate === undefined ? false : scatterUpdate; + init = init === undefined ? false : init; + var args = { selectedPatientIDs: patientIDs }; + + rServeService.startScriptExecution({ + taskType: 'run', + arguments: args + }).then( + function (response) { + var results = JSON.parse(response.result.artifacts.value); + if (init) { + scope.data = results; + } else { + setData(results); + if (scatterUpdate) { + updateScatterplot(); + } + updateRegressionLine(); + updateLegend(); + updateHistogram(); + } + }, + function (response) { + console.error(' Failure: ' + response.statusText); + } + ); + } - function dragmove() { - legend - .style('left', smartRUtils.mouseX(root) + 'px') - .style('top', smartRUtils.mouseY(root) + 'px'); - } + var svg = d3.select(root).append('svg') + .attr('width', width + margin.left + margin.right) + .attr('height', height + margin.top + margin.bottom) + .append('g') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') + .on('contextmenu', function() { + d3.event.preventDefault(); + contextMenu + .style('visibility', 'visible') + .style('left', smartRUtils.mouseX(root) + 'px') + .style('top', smartRUtils.mouseY(root) + 'px'); + }); - var drag = d3.behavior.drag() - .on('drag', dragmove); - - var legend = d3.select(root).append('div') - .attr('class', 'legend') - .style('left', 0) - .style('top', $('#scatterplot').offsetTop + 'px') - .call(drag); - - svg.append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0, 0)') - .call(d3.svg.axis() - .scale(x) - .ticks(10) - .tickFormat('') - .innerTickSize(height) - .orient('bottom')); - - svg.append('text') - .attr('class', 'axisLabels') - .attr('transform', 'translate(' + width / 2 + ',' + - margin.top / 2 + ')') - .text(smartRUtils.shortenConcept(xArrLabel)); - - svg.append('g') - .attr('class', 'y axis') - .attr('transform', 'translate(' + width + ',' + 0 + ')') - .call(d3.svg.axis() - .scale(y) - .ticks(10) - .tickFormat('') - .innerTickSize(width) - .orient('left')); - - svg.append('text') - .attr('class', 'axisLabels') - .attr('transform', 'translate(' + (width + margin.right / 2) + ',' + height / 2 + ')rotate(90)') - .text(smartRUtils.shortenConcept(yArrLabel)); - - svg.append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(' + 0 + ',' + height + ')') - .call(d3.svg.axis() - .scale(x) - .orient('top')); - - svg.append('g') - .attr('class', 'y axis') - .attr('transform', 'translate(' + 0 + ',' + 0 + ')') - .call(d3.svg.axis() - .scale(y) - .orient('right')); - - function excludeSelection() { - var remainingPatientIDs = d3.selectAll('.point:not(.selected)').map(function(d) { return d.patientID; }); - updateStatistics(remainingPatientIDs, true); - } + var tooltip = d3.select(root).append('div') + .attr('class', 'tooltip') + .style('visibility', 'hidden'); - function zoomSelection() { - if (d3.selectAll('.point.selected').size() < 2) { - alert('Please select at least two elements before zooming!'); - return; + function dragmove() { + legend + .style('left', smartRUtils.mouseX(root) + 'px') + .style('top', smartRUtils.mouseY(root) + 'px'); } - var selectedPatientIDs = d3.selectAll('.point.selected').map(function(d) { return d.patientID; }); - updateStatistics(selectedPatientIDs, false, true); - } - var ctxHtml = '
\ -
\ - '; - - var contextMenu = d3.select(root).append('div') - .attr('class', 'contextMenu') - .style('visibility', 'hidden') - .html(ctxHtml); - $('#zoomButton').on('click', function() { - contextMenu.style('visibility', 'hidden'); - zoomSelection(); - }); - $('#excludeButton').on('click', function() { - contextMenu.style('visibility', 'hidden'); - excludeSelection(); - }); - $('#resetButton').on('click', function() { - contextMenu.style('visibility', 'hidden'); - reset(); - }); - - function updateSelection() { - var extent = brush.extent(); - var x0 = x.invert(extent[0][0]); - var x1 = x.invert(extent[1][0]); - var y0 = y.invert(extent[0][1]); - var y1 = y.invert(extent[1][1]); - svg.selectAll('.point') - .classed('selected', false) - .style('fill', function(d) { return getColor(d.annotation); }) - .style('stroke', 'white') - .filter(function(d) { - return x0 <= d.x && d.x <= x1 && y1 <= d.y && d.y <= y0; - }) - .classed('selected', true) - .style('fill', 'white') - .style('stroke', function(d) { return getColor(d.annotation); }); - } + var drag = d3.behavior.drag() + .on('drag', dragmove); + + var legend = d3.select(root).append('div') + .attr('class', 'legend') + .style('left', 0) + .style('top', $('#scatterplot').offsetTop + 'px') + .call(drag); + + svg.append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0, 0)') + .call(d3.svg.axis() + .scale(x) + .ticks(10) + .tickFormat('') + .innerTickSize(height) + .orient('bottom')); + + svg.append('text') + .attr('class', 'axisLabels') + .attr('transform', 'translate(' + width / 2 + ',' + - margin.top / 2 + ')') + .text(smartRUtils.shortenConcept(xArrLabel)); + + svg.append('g') + .attr('class', 'y axis') + .attr('transform', 'translate(' + width + ',' + 0 + ')') + .call(d3.svg.axis() + .scale(y) + .ticks(10) + .tickFormat('') + .innerTickSize(width) + .orient('left')); + + svg.append('text') + .attr('class', 'axisLabels') + .attr('transform', 'translate(' + (width + margin.right / 2) + ',' + height / 2 + ')rotate(90)') + .text(smartRUtils.shortenConcept(yArrLabel)); + + svg.append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(' + 0 + ',' + height + ')') + .call(d3.svg.axis() + .scale(x) + .orient('top')); + + svg.append('g') + .attr('class', 'y axis') + .attr('transform', 'translate(' + 0 + ',' + 0 + ')') + .call(d3.svg.axis() + .scale(y) + .orient('right')); + + function excludeSelection() { + var remainingPatientIDs = d3.selectAll('.point:not(.selected)').map(function(d) { return d.patientID; }); + updateStatistics(remainingPatientIDs, true); + } - var brush = d3.svg.brush() - .x(d3.scale.identity().domain([0, width])) - .y(d3.scale.identity().domain([0, height])) - .on('brushend', function() { - contextMenu - .style('visibility', 'hidden') - .style('top', -100 + 'px'); - updateSelection(); + function zoomSelection() { + if (d3.selectAll('.point.selected').size() < 2) { + alert('Please select at least two elements before zooming!'); + return; + } var selectedPatientIDs = d3.selectAll('.point.selected').map(function(d) { return d.patientID; }); - updateStatistics(selectedPatientIDs); - }); + updateStatistics(selectedPatientIDs, false, true); + } - svg.append('g') - .attr('class', 'brush') - .on('mousedown', function() { - return d3.event.button === 2 ? d3.event.stopImmediatePropagation() : null; - }) - .call(brush); + var ctxHtml = '
' + + '
' + + ''; + + var contextMenu = d3.select(root).append('div') + .attr('class', 'contextMenu') + .style('visibility', 'hidden') + .html(ctxHtml); + $('#zoomButton').on('click', function() { + contextMenu.style('visibility', 'hidden'); + zoomSelection(); + }); + $('#excludeButton').on('click', function() { + contextMenu.style('visibility', 'hidden'); + excludeSelection(); + }); + $('#resetButton').on('click', function() { + contextMenu.style('visibility', 'hidden'); + reset(); + }); - function getColor(annotation) { - return annotation ? colors[annotations.indexOf(annotation)] : 'black'; - } + function updateSelection() { + var extent = brush.extent(); + var x0 = x.invert(extent[0][0]); + var x1 = x.invert(extent[1][0]); + var y0 = y.invert(extent[0][1]); + var y1 = y.invert(extent[1][1]); + svg.selectAll('.point') + .classed('selected', false) + .style('fill', function(d) { return getColor(d.annotation); }) + .style('stroke', 'white') + .filter(function(d) { + return x0 <= d.x && d.x <= x1 && y1 <= d.y && d.y <= y0; + }) + .classed('selected', true) + .style('fill', 'white') + .style('stroke', function(d) { return getColor(d.annotation); }); + } - function updateScatterplot() { - var point = svg.selectAll('.point') - .data(points, function(d) { return d.patientID; }); - - point.enter() - .append('circle') - .attr('class', 'point') - .attr('cx', function(d) { return x(d.x); }) - .attr('cy', function(d) { return y(d.y); }) - .attr('r', 5) - .style('fill', function(d) { return getColor(d.annotation); }) - .on('mouseover', function(d) { - d3.select(this).style('fill', '#FF0000'); - tooltip - .style('left', 10 + smartRUtils.mouseX(root) + 'px') - .style('top', 10 + smartRUtils.mouseY(root) + 'px') - .style('visibility', 'visible') - .html(smartRUtils.shortenConcept(xArrLabel) + ': ' + d.x + '
' + - smartRUtils.shortenConcept(yArrLabel) + ': ' + d.y + '
' + - 'Patient ID: ' + d.patientID + '
' + - (d.annotation ? 'Tag: ' + d.annotation : '')); - }) - .on('mouseout', function() { - var p = d3.select(this); - p.style('fill', function(d) { return p.classed('selected') ? '#FFFFFF' : getColor(d.annotation) }); - tooltip.style('visibility', 'hidden'); + var brush = d3.svg.brush() + .x(d3.scale.identity().domain([0, width])) + .y(d3.scale.identity().domain([0, height])) + .on('brushend', function() { + contextMenu + .style('visibility', 'hidden') + .style('top', -100 + 'px'); + updateSelection(); + var selectedPatientIDs = d3.selectAll('.point.selected').map(function(d) { return d.patientID; }); + updateStatistics(selectedPatientIDs); }); - point.exit() - .classed('selected', false) - .transition() - .duration(animationDuration) - .attr('r', 0) - .remove(); - } + svg.append('g') + .attr('class', 'brush') + .on('mousedown', function() { + return d3.event.button === 2 ? d3.event.stopImmediatePropagation() : null; + }) + .call(brush); - function updateHistogram() { - var bottomHistData = d3.layout.histogram() - .bins(bins)(points.map(function(d) { return d.x; })); - var leftHistData = d3.layout.histogram() - .bins(bins)(points.map(function(d) { return d.y; })); - - var bottomHistHeightScale = d3.scale.linear() - .domain([0, bottomHistData.max(function(d) { return d.y; })]) - .range([1, bottomHistHeight]); - var leftHistHeightScale = d3.scale.linear() - .domain([0, leftHistData.max(function(d) { return d.y; })]) - .range([2, leftHistHeight]); - - var bottomHistGroup = svg.selectAll('.bar.bottom') - .data(Array(bins).fill().map(function(_, i) { return i; })); - var bottomHistGroupEnter = bottomHistGroup.enter() - .append('g') - .attr('class', 'bar bottom'); - var bottomHistGroupExit = bottomHistGroup.exit(); - - bottomHistGroupEnter.append('rect') - .attr('y', height + 1); - bottomHistGroup.selectAll('rect') - .transition() - .delay(function(d) { return d * 25; }) - .duration(animationDuration) - .attr('x', function(d) { return x(bottomHistData[d].x); }) - .attr('width', function() { return (x(maxX) - x(minX)) / bins; }) - .attr('height', function(d) { return bottomHistHeightScale(bottomHistData[d].y) - 1; }); - bottomHistGroupExit.selectAll('rect') - .transition() - .duration(animationDuration) - .attr('height', 0); - - bottomHistGroupEnter.append('text') - .attr('dy', '.35em') - .attr('text-anchor', 'middle'); - bottomHistGroup.selectAll('text') - .text(function(d) { return bottomHistData[d].y || ''; }) - .transition() - .delay(function(d) { return d * 25; }) - .duration(animationDuration) - .attr('x', function(d) { return x(bottomHistData[d].x) + (x(maxX) - x(minX)) / bins / 2; }) - .attr('y', function(d) { return height + bottomHistHeightScale(bottomHistData[d].y) - 10; }); - bottomHistGroupExit.selectAll('text') - .text(''); - - var leftHistGroup = svg.selectAll('.bar.left') - .data(Array(bins).fill().map(function(_, i) { return i; })); - var leftHistGroupEnter = leftHistGroup.enter() - .append('g') - .attr('class', 'bar left'); - var leftHistGroupExit = leftHistGroup.exit(); - - leftHistGroupEnter.append('rect'); - leftHistGroup.selectAll('rect') - .transition() - .delay(function(d) { return d * 25; }) - .duration(animationDuration) - .attr('x', function(d) { return - leftHistHeightScale(leftHistData[d].y) + 1; }) - .attr('y', function(d) { return y(leftHistData[d].x) - (y(minY) - y(maxY))/ bins; }) - .attr('width', function(d) { return leftHistHeightScale(leftHistData[d].y) - 2; }) - .attr('height', function() { return (y(minY) - y(maxY))/ bins; }); - leftHistGroupExit.selectAll('rect') - .transition() - .duration(animationDuration) - .attr('height', 0); - - leftHistGroupEnter.append('text') - .attr('dy', '.35em') - .attr('text-anchor', 'middle'); - leftHistGroup.selectAll('text') - .text(function(d) { return leftHistData[d].y || ''; }) - .transition() - .delay(function(d) { return d * 25; }) - .duration(animationDuration) - .attr('x', function(d) { return - leftHistHeightScale(leftHistData[d].y) + 10; }) - .attr('y', function(d) { return y(leftHistData[d].x) - (y(minY) - y(maxY))/ bins / 2; }); - leftHistGroupExit.selectAll('text') - .text(''); - } + function getColor(annotation) { + return annotation ? colors[annotations.indexOf(annotation)] : 'black'; + } - function updateLegend() { - var html = 'Correlation Coefficient: ' + correlation + '
' + - 'p-value: ' + pvalue + '
' + - 'Method: ' + method + '

' + - 'Selected: ' + d3.selectAll('.point.selected').size() || d3.selectAll('.point').size() + '
' + - 'Displayed: ' + d3.selectAll('.point').size() + '

'; + function updateScatterplot() { + var point = svg.selectAll('.point') + .data(points, function(d) { return d.patientID; }); + + point.enter() + .append('circle') + .attr('class', 'point') + .attr('cx', function(d) { return x(d.x); }) + .attr('cy', function(d) { return y(d.y); }) + .attr('r', 5) + .style('fill', function(d) { return getColor(d.annotation); }) + .on('mouseover', function(d) { + d3.select(this).style('fill', '#FF0000'); + tooltip + .style('left', 10 + smartRUtils.mouseX(root) + 'px') + .style('top', 10 + smartRUtils.mouseY(root) + 'px') + .style('visibility', 'visible') + .html(smartRUtils.shortenConcept(xArrLabel) + ': ' + d.x + '
' + + smartRUtils.shortenConcept(yArrLabel) + ': ' + d.y + '
' + + 'Patient ID: ' + d.patientID + '
' + + (d.annotation ? 'Tag: ' + d.annotation : '')); + }) + .on('mouseout', function() { + var p = d3.select(this); + p.style('fill', function(d) { + return p.classed('selected') ? '#FFFFFF' : getColor(d.annotation); + }); + tooltip.style('visibility', 'hidden'); + }); + + point.exit() + .classed('selected', false) + .transition() + .duration(animationDuration) + .attr('r', 0) + .remove(); + } - html = html + '

Default

'; + function updateHistogram() { + var bottomHistData = d3.layout.histogram() + .bins(bins)(points.map(function(d) { return d.x; })); + var leftHistData = d3.layout.histogram() + .bins(bins)(points.map(function(d) { return d.y; })); + + var bottomHistHeightScale = d3.scale.linear() + .domain([0, bottomHistData.max(function(d) { return d.y; })]) + .range([1, bottomHistHeight]); + var leftHistHeightScale = d3.scale.linear() + .domain([0, leftHistData.max(function(d) { return d.y; })]) + .range([2, leftHistHeight]); + + var bottomHistGroup = svg.selectAll('.bar.bottom') + .data(Array(bins).fill().map(function(_, i) { return i; })); + var bottomHistGroupEnter = bottomHistGroup.enter() + .append('g') + .attr('class', 'bar bottom'); + var bottomHistGroupExit = bottomHistGroup.exit(); + + bottomHistGroupEnter.append('rect') + .attr('y', height + 1); + bottomHistGroup.selectAll('rect') + .transition() + .delay(function(d) { return d * 25; }) + .duration(animationDuration) + .attr('x', function(d) { return x(bottomHistData[d].x); }) + .attr('width', function() { return (x(maxX) - x(minX)) / bins; }) + .attr('height', function(d) { return bottomHistHeightScale(bottomHistData[d].y) - 1; }); + bottomHistGroupExit.selectAll('rect') + .transition() + .duration(animationDuration) + .attr('height', 0); + + bottomHistGroupEnter.append('text') + .attr('dy', '.35em') + .attr('text-anchor', 'middle'); + bottomHistGroup.selectAll('text') + .text(function(d) { return bottomHistData[d].y || ''; }) + .transition() + .delay(function(d) { return d * 25; }) + .duration(animationDuration) + .attr('x', function(d) { return x(bottomHistData[d].x) + (x(maxX) - x(minX)) / bins / 2; }) + .attr('y', function(d) { return height + bottomHistHeightScale(bottomHistData[d].y) - 10; }); + bottomHistGroupExit.selectAll('text') + .text(''); + + var leftHistGroup = svg.selectAll('.bar.left') + .data(Array(bins).fill().map(function(_, i) { return i; })); + var leftHistGroupEnter = leftHistGroup.enter() + .append('g') + .attr('class', 'bar left'); + var leftHistGroupExit = leftHistGroup.exit(); + + leftHistGroupEnter.append('rect'); + leftHistGroup.selectAll('rect') + .transition() + .delay(function(d) { return d * 25; }) + .duration(animationDuration) + .attr('x', function(d) { return - leftHistHeightScale(leftHistData[d].y) + 1; }) + .attr('y', function(d) { return y(leftHistData[d].x) - (y(minY) - y(maxY))/ bins; }) + .attr('width', function(d) { return leftHistHeightScale(leftHistData[d].y) - 2; }) + .attr('height', function() { return (y(minY) - y(maxY))/ bins; }); + leftHistGroupExit.selectAll('rect') + .transition() + .duration(animationDuration) + .attr('height', 0); + + leftHistGroupEnter.append('text') + .attr('dy', '.35em') + .attr('text-anchor', 'middle'); + leftHistGroup.selectAll('text') + .text(function(d) { return leftHistData[d].y || ''; }) + .transition() + .delay(function(d) { return d * 25; }) + .duration(animationDuration) + .attr('x', function(d) { return - leftHistHeightScale(leftHistData[d].y) + 10; }) + .attr('y', function(d) { return y(leftHistData[d].x) - (y(minY) - y(maxY))/ bins / 2; }); + leftHistGroupExit.selectAll('text') + .text(''); + } - annotations.forEach(function(annotation) { - html += '

' + annotation + '

'; - }); + function updateLegend() { + var html = 'Correlation Coefficient: ' + correlation + '
' + + 'p-value: ' + pvalue + '
' + + 'Method: ' + method + '

' + + 'Selected: ' + d3.selectAll('.point.selected').size() || d3.selectAll('.point').size() + '
' + + 'Displayed: ' + d3.selectAll('.point').size() + '

'; - legend.html(html); - } + html = html + '

Default

'; - function updateRegressionLine() { - var regressionLine = svg.selectAll('.regressionLine') - .data(regLineSlope === 'NA' ? [] : [0], function(d) { return d }); - regressionLine.enter() - .append('line') - .attr('class', 'regressionLine') - .on('mouseover', function () { - d3.select(this).attr('stroke', 'red'); - tooltip - .style('visibility', 'visible') - .html('slope: ' + regLineSlope + '
intercept: ' + regLineYIntercept) - .style('left', smartRUtils.mouseX(root) + 'px') - .style('top', smartRUtils.mouseY(root) + 'px'); - }) - .on('mouseout', function () { - d3.select(this).attr('stroke', 'orange'); - tooltip.style('visibility', 'hidden'); + annotations.forEach(function(annotation) { + html += '

' + annotation + '

'; }); - regressionLine.transition() - .duration(animationDuration) - .attr('x1', x(minX)) - .attr('y1', y(regLineYIntercept + regLineSlope * minX)) - .attr('x2', x(maxX)) - .attr('y2', y(regLineYIntercept + regLineSlope * maxX)); + legend.html(html); + } - regressionLine.exit() - .remove(); - } + function updateRegressionLine() { + var regressionLine = svg.selectAll('.regressionLine') + .data(regLineSlope === 'NA' ? [] : [0], function(d) { return d; }); + regressionLine.enter() + .append('line') + .attr('class', 'regressionLine') + .on('mouseover', function () { + d3.select(this).attr('stroke', 'red'); + tooltip + .style('visibility', 'visible') + .html('slope: ' + regLineSlope + '
intercept: ' + regLineYIntercept) + .style('left', smartRUtils.mouseX(root) + 'px') + .style('top', smartRUtils.mouseY(root) + 'px'); + }) + .on('mouseout', function () { + d3.select(this).attr('stroke', 'orange'); + tooltip.style('visibility', 'hidden'); + }); + + regressionLine.transition() + .duration(animationDuration) + .attr('x1', x(minX)) + .attr('y1', y(regLineYIntercept + regLineSlope * minX)) + .attr('x2', x(maxX)) + .attr('y2', y(regLineYIntercept + regLineSlope * maxX)); + + regressionLine.exit() + .remove(); + } - function reset() { - updateStatistics([], false, true); - } + function reset() { + updateStatistics([], false, true); + } - updateScatterplot(); - updateHistogram(); - updateRegressionLine(); - updateLegend(); - } + updateScatterplot(); + updateHistogram(); + updateRegressionLine(); + updateLegend(); + } -}]); + }]); diff --git a/web-app/js/smartR/_angular/viz/d3Heatmap.js b/web-app/js/smartR/_angular/viz/d3Heatmap.js index b2517af..1a3d35f 100644 --- a/web-app/js/smartR/_angular/viz/d3Heatmap.js +++ b/web-app/js/smartR/_angular/viz/d3Heatmap.js @@ -1,1495 +1,1504 @@ //# sourceURL=d3Heatmap.js -window.smartRApp.directive('heatmapPlot', ['smartRUtils', 'rServeService', function(smartRUtils, rServeService) { - - return { - restrict: 'E', - scope: { - data: '=', - width: '@', - height: '@', - params: '=' - }, - link: function (scope, element, attrs) { - - /** - * Watch data model (which is only changed by ajax calls when we want to (re)draw everything) - */ - scope.$watch('data', function (newValue, oldValue) { - $(element[0]).empty(); - if (angular.isArray(newValue.fields)) { - createHeatmap(scope.data, element[0], scope.params); +'use strict'; + +window.smartRApp.directive('heatmapPlot', [ + 'smartRUtils', + 'controlElements', + function(smartRUtils, controlElements) { + + return { + restrict: 'E', + scope: { + data: '=', + width: '@', + height: '@', + params: '=' + }, + link: function(scope, element) { + + /** + * Watch data model (which is only changed by ajax calls when we want to (re)draw everything) + */ + scope.$watch('data', function(newValue) { + $(element[0]).empty(); + if (angular.isArray(newValue.fields)) { + createHeatmap(scope.data, element[0], scope.params); + } + }, true); + } + }; + + function createHeatmap(data, root, params) { + var animationDuration = 1500; + var extraFields = data.extraFields === undefined ? [] : data.extraFields; + var features = data.features === undefined ? [] : data.features; + var fields = data.fields; + var significanceValues = data.significanceValues; + var logfoldValues = data.logfoldValues; + var ttestValues = data.ttestValues; + var pvalValues = data.pvalValues; + var adjpvalValues = data.adjpvalValues; + var bvalValues = data.bvalValues; + var ranking = data.ranking[0]; + var patientIDs = data.patientIDs; + var uids = data.uids; + var numberOfClusteredColumns = data.numberOfClusteredColumns[0]; + var numberOfClusteredRows = data.numberOfClusteredRows[0]; + // var warning = data.warnings === undefined ? '' : data.warnings; + var maxRows = data.maxRows[0]; + + var rowClustering = true; + var colClustering = true; + + var originalPatientIDs = patientIDs.slice(); + var originalUIDs = uids.slice(); + + var tmpAnimationDuration = animationDuration; + function switchAnimation(checked) { + if (! checked) { + tmpAnimationDuration = animationDuration; + animationDuration = 0; + } else { + animationDuration = tmpAnimationDuration; } - }, true); - } - }; - - function createHeatmap(data, root, params) { - var animationDuration = 1500; - var extraFields = data.extraFields === undefined ? [] : data.extraFields; - var features = data.features === undefined ? [] : data.features; - var fields = data.fields; - var significanceValues = data.significanceValues; - var logfoldValues = data.logfoldValues; - var ttestValues = data.ttestValues; - var pvalValues = data.pvalValues; - var adjpvalValues = data.adjpvalValues; - var bvalValues = data.bvalValues; - var ranking = data.ranking[0]; - var patientIDs = data.patientIDs; - var uids = data.uids; - var numberOfClusteredColumns = data.numberOfClusteredColumns[0]; - var numberOfClusteredRows = data.numberOfClusteredRows[0]; - var warning = data.warnings === undefined ? '' : data.warnings; - var maxRows = data.maxRows[0]; - - var rowClustering = true; - var colClustering = true; - - var originalPatientIDs = patientIDs.slice(); - var originalUIDs = uids.slice(); - - var tmpAnimationDuration = animationDuration; - function switchAnimation(checked) { - if (! checked) { - tmpAnimationDuration = animationDuration; - animationDuration = 0; - } else { - animationDuration = tmpAnimationDuration; } - } - var gridFieldWidth = 20; - var gridFieldHeight = 20; - var dendrogramHeight = 300; - var histogramHeight = 200; - var legendWidth = 200; - var legendHeight = 40; - var buttonWidth = 200; - var buttonHeight = 40; - var buttonPadding = 20; - - var margin = { - top: gridFieldHeight * 2 + 100 + features.length * gridFieldHeight / 2 + dendrogramHeight, - right: gridFieldWidth + 300 + dendrogramHeight, - bottom: 10, - left: histogramHeight + 250 - }; + var gridFieldWidth = 20; + var gridFieldHeight = 20; + var dendrogramHeight = 300; + var histogramHeight = 200; + var legendWidth = 200; + var legendHeight = 40; + var buttonWidth = 200; + var buttonHeight = 40; + var buttonPadding = 20; + + var margin = { + top: gridFieldHeight * 2 + 100 + features.length * gridFieldHeight / 2 + dendrogramHeight, + right: gridFieldWidth + 300 + dendrogramHeight, + bottom: 10, + left: histogramHeight + 250 + }; - var width = gridFieldWidth * patientIDs.length; - var height = gridFieldHeight * uids.length; + var width = gridFieldWidth * patientIDs.length; + var height = gridFieldHeight * uids.length; - var selectedPatientIDs = []; + var selectedPatientIDs = []; - var scale = null; - var histogramScale = null; + var scale = null; + var histogramScale = null; - function setScales() { - scale = d3.scale.linear() - .domain(d3.extent(significanceValues)) - .range((ranking === 'pval' || ranking === 'adjpval') ? [histogramHeight, 0] : [0, histogramHeight]); + function setScales() { + scale = d3.scale.linear() + .domain(d3.extent(significanceValues)) + .range((ranking === 'pval' || ranking === 'adjpval') ? [histogramHeight, 0] : [0, histogramHeight]); - histogramScale = function (value) { - return (ranking === 'ttest' || ranking === 'logfold') ? scale(Math.abs(value)) : scale(value); - }; - } - setScales(); - - function getInternalSortValue(value) { - switch (ranking) { - case 'pval': - case 'adjpval': - return 1 - value; - default: - return value; + histogramScale = function (value) { + return (ranking === 'ttest' || ranking === 'logfold') ? scale(Math.abs(value)) : scale(value); + }; } - } + setScales(); - var heatmap = d3.select(root).append('svg') - .attr('width', (width + margin.left + margin.right) * 4) - .attr('height', (height + margin.top + margin.bottom) * 4) - .attr('class', 'visualization') - .append('g') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - - function adjustDimensions() { - // gridFieldWidth/gridFieldHeight are adjusted outside as the zoom changes - $(heatmap[0]).closest('svg') - .attr('width', margin.left + margin.right + (gridFieldWidth * patientIDs.length)) - .attr('height', margin.top + margin.bottom + (gridFieldHeight * uids.length)); - } + function getInternalSortValue(value) { + switch (ranking) { + case 'pval': + case 'adjpval': + return 1 - value; + default: + return value; + } + } - adjustDimensions(); - - var tooltip = d3.select(root).append('div') - .attr('class', 'tooltip text') - .style('visibility', 'hidden'); - - var featureItems = heatmap.append('g'); - var squareItems = heatmap.append('g'); - var colSortItems = heatmap.append('g'); - var selectItems = heatmap.append('g'); - var patientIDItems = heatmap.append('g'); - var rowSortItems = heatmap.append('g'); - var significanceSortItems = heatmap.append('g'); - var labelItems = heatmap.append('g'); - var barItems = heatmap.append('g'); - var legendItems = heatmap.append('g'); - var warningDiv = $('#heim-heatmap-warnings').append('strong') - .text(warning); - - var uniqueSortedZscores = fields.map(function(d) { - return parseFloat(d.ZSCORE); - }); - uniqueSortedZscores.sort(function(a, b) { return a - b; }); - uniqueSortedZscores.filter(function(d, i) { - return !i || d != uniqueSortedZscores[i - 1]; - }); - - - function updateHeatmap() { - var square = squareItems.selectAll('.square') - .data(fields, function (d) { - return 'patientID-' + d.PATIENTID + '-uid-' + d.UID; - }); + var heatmap = d3.select(root).append('svg') + .attr('width', (width + margin.left + margin.right) * 4) + .attr('height', (height + margin.top + margin.bottom) * 4) + .attr('class', 'visualization') + .append('g') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + function adjustDimensions() { + // gridFieldWidth/gridFieldHeight are adjusted outside as the zoom changes + $(heatmap[0]).closest('svg') + .attr('width', margin.left + margin.right + (gridFieldWidth * patientIDs.length)) + .attr('height', margin.top + margin.bottom + (gridFieldHeight * uids.length)); + } - square.enter() - .append('rect') - .attr('class', function (d) { - return 'square patientID-' + smartRUtils.makeSafeForCSS(d.PATIENTID) + - ' uid-' + smartRUtils.makeSafeForCSS(d.UID); - }) - .attr('x', function (d) { - return patientIDs.indexOf(d.PATIENTID) * gridFieldWidth; - }) - .attr('y', function (d) { - return uids.indexOf(d.UID) * gridFieldHeight; - }) - .attr('width', gridFieldWidth) - .attr('height', gridFieldHeight) - .attr('rx', 0) - .attr('ry', 0) - .style('fill', 'white') - .on('mouseover', function (d) { - d3.select('.patientID.patientID-' + smartRUtils.makeSafeForCSS(d.PATIENTID)) - .classed('highlight', true); - d3.select('.uid.uid-' + smartRUtils.makeSafeForCSS(d.UID)) - .classed('highlight', true); - - var html = ''; - for (var key in d) { - html += key + ': ' + d[key] + '
'; - } - tooltip - .style('visibility', 'visible') - .html(html) - .style('left', smartRUtils.mouseX(root) + 'px') - .style('top', smartRUtils.mouseY(root) + 'px'); - }) - .on('mouseout', function () { - d3.selectAll('.patientID').classed('highlight', false); - d3.selectAll('.uid').classed('highlight', false); - tooltip.style('visibility', 'hidden'); - }); - - square.transition() - .duration(animationDuration) - .attr('x', function (d) { - return patientIDs.indexOf(d.PATIENTID) * gridFieldWidth; - }) - .attr('y', function (d) { - return uids.indexOf(d.UID) * gridFieldHeight; - }) - .attr('width', gridFieldWidth) - .attr('height', gridFieldHeight); - - var colSortText = colSortItems.selectAll('.colSortText') - .data(patientIDs, function (d) { - return d; - }); + adjustDimensions(); - colSortText.enter() - .append('text') - .attr('class', 'text colSortText') - .attr('x', function (d, i) { - return i * gridFieldWidth + 0.5 * gridFieldWidth; - }) - .attr('y', -2 - gridFieldHeight + 0.5 * gridFieldHeight) - .attr('dy', '0.35em') - .attr('text-anchor', 'middle') - .text('↑↓'); - - colSortText.transition() - .duration(animationDuration) - .attr('x', function (d, i) { - return i * gridFieldWidth + 0.5 * gridFieldWidth; - }) - .attr('y', -2 - gridFieldHeight + 0.5 * gridFieldHeight); - - var colSortBox = colSortItems.selectAll('.colSortBox') - .data(patientIDs, function (d) { - return d; - }); + var tooltip = d3.select(root).append('div') + .attr('class', 'tooltip text') + .style('visibility', 'hidden'); + + var featureItems = heatmap.append('g'); + var squareItems = heatmap.append('g'); + var colSortItems = heatmap.append('g'); + var selectItems = heatmap.append('g'); + var patientIDItems = heatmap.append('g'); + var rowSortItems = heatmap.append('g'); + var significanceSortItems = heatmap.append('g'); + var labelItems = heatmap.append('g'); + var barItems = heatmap.append('g'); + var legendItems = heatmap.append('g'); + // var warningDiv = $('#heim-heatmap-warnings').append('strong') + // .text(warning); + + var uniqueSortedZscores = fields.map(function(d) { + return parseFloat(d.ZSCORE); + }); + uniqueSortedZscores.sort(function(a, b) { return a - b; }); + uniqueSortedZscores.filter(function(d, i) { + return !i || d !== uniqueSortedZscores[i - 1]; + }); - function getValueForSquareSorting(patientID, uid) { - var square = d3.select('.square' + '.patientID-' + smartRUtils.makeSafeForCSS(patientID) + - '.uid-' + smartRUtils.makeSafeForCSS(uid)); - return square[0][0] != null ? square.property('__data__').ZSCORE : Number.NEGATIVE_INFINITY; - } - function isSorted(arr) { - return arr.every(function (d, i) { - return i === arr.length - 1 || arr[i][1] >= arr[i + 1][1]; - }); - } + function updateHeatmap() { + var square = squareItems.selectAll('.square') + .data(fields, function (d) { + return 'patientID-' + d.PATIENTID + '-uid-' + d.UID; + }); + + square.enter() + .append('rect') + .attr('class', function (d) { + return 'square patientID-' + smartRUtils.makeSafeForCSS(d.PATIENTID) + + ' uid-' + smartRUtils.makeSafeForCSS(d.UID); + }) + .attr('x', function (d) { + return patientIDs.indexOf(d.PATIENTID) * gridFieldWidth; + }) + .attr('y', function (d) { + return uids.indexOf(d.UID) * gridFieldHeight; + }) + .attr('width', gridFieldWidth) + .attr('height', gridFieldHeight) + .attr('rx', 0) + .attr('ry', 0) + .style('fill', 'white') + .on('mouseover', function (d) { + d3.select('.patientID.patientID-' + smartRUtils.makeSafeForCSS(d.PATIENTID)) + .classed('highlight', true); + d3.select('.uid.uid-' + smartRUtils.makeSafeForCSS(d.UID)) + .classed('highlight', true); + + var html = ''; + for (var key in d) { + if (d.hasOwnProperty(key)) { + html += key + ': ' + d[key] + '
'; + } + } + tooltip + .style('visibility', 'visible') + .html(html) + .style('left', smartRUtils.mouseX(root) + 'px') + .style('top', smartRUtils.mouseY(root) + 'px'); + }) + .on('mouseout', function () { + d3.selectAll('.patientID').classed('highlight', false); + d3.selectAll('.uid').classed('highlight', false); + tooltip.style('visibility', 'hidden'); + }); + + square.transition() + .duration(animationDuration) + .attr('x', function (d) { + return patientIDs.indexOf(d.PATIENTID) * gridFieldWidth; + }) + .attr('y', function (d) { + return uids.indexOf(d.UID) * gridFieldHeight; + }) + .attr('width', gridFieldWidth) + .attr('height', gridFieldHeight); + + var colSortText = colSortItems.selectAll('.colSortText') + .data(patientIDs, function (d) { + return d; + }); + + colSortText.enter() + .append('text') + .attr('class', 'text colSortText') + .attr('x', function (d, i) { + return i * gridFieldWidth + 0.5 * gridFieldWidth; + }) + .attr('y', -2 - gridFieldHeight + 0.5 * gridFieldHeight) + .attr('dy', '0.35em') + .attr('text-anchor', 'middle') + .text('↑↓'); + + colSortText.transition() + .duration(animationDuration) + .attr('x', function (d, i) { + return i * gridFieldWidth + 0.5 * gridFieldWidth; + }) + .attr('y', -2 - gridFieldHeight + 0.5 * gridFieldHeight); + + var colSortBox = colSortItems.selectAll('.colSortBox') + .data(patientIDs, function (d) { + return d; + }); + + function getValueForSquareSorting(patientID, uid) { + var square = d3.select('.square' + '.patientID-' + smartRUtils.makeSafeForCSS(patientID) + + '.uid-' + smartRUtils.makeSafeForCSS(uid)); + return square[0][0] ? square.property('__data__').ZSCORE : Number.NEGATIVE_INFINITY; + } - colSortBox.enter() - .append('rect') - .attr('class', 'box colSortBox') - .attr('x', function (d, i) { - return i * gridFieldWidth; - }) - .attr('y', -2 - gridFieldHeight) - .attr('width', gridFieldWidth) - .attr('height', gridFieldHeight) - .on('click', function (patientID) { - d3.selectAll('.box').classed('sortedBy', false); - d3.select(this).classed('sortedBy', true); - - var rowValues = uids.map(function (uid, idx) { - return [idx, getValueForSquareSorting(patientID, uid)]; + function isSorted(arr) { + return arr.every(function (d, i) { + return i === arr.length - 1 || arr[i][1] >= arr[i + 1][1]; }); - if (isSorted(rowValues)) { - rowValues.sort(function (a, b) { - return a[1] - b[1]; + } + + colSortBox.enter() + .append('rect') + .attr('class', 'box colSortBox') + .attr('x', function (d, i) { + return i * gridFieldWidth; + }) + .attr('y', -2 - gridFieldHeight) + .attr('width', gridFieldWidth) + .attr('height', gridFieldHeight) + .on('click', function (patientID) { + d3.selectAll('.box').classed('sortedBy', false); + d3.select(this).classed('sortedBy', true); + + var rowValues = uids.map(function (uid, idx) { + return [idx, getValueForSquareSorting(patientID, uid)]; }); - } else { - rowValues.sort(function (a, b) { - return b[1] - a[1]; + if (isSorted(rowValues)) { + rowValues.sort(function (a, b) { + return a[1] - b[1]; + }); + } else { + rowValues.sort(function (a, b) { + return b[1] - a[1]; + }); + } + var sortValues = rowValues.map(function (rowValue) { + return rowValue[0]; }); - } - var sortValues = rowValues.map(function (rowValue) { - return rowValue[0]; + updateRowOrder(sortValues); }); - updateRowOrder(sortValues); - }); - - colSortBox.transition() - .duration(animationDuration) - .attr('x', function (d, i) { - return i * gridFieldWidth; - }) - .attr('y', -2 - gridFieldHeight) - .attr('width', gridFieldWidth) - .attr('height', gridFieldHeight); - - var rowSortText = rowSortItems.selectAll('.rowSortText') - .data(uids, function (d) { - return d; - }); - rowSortText.enter() - .append('text') - .attr('class', 'text rowSortText') - .attr('transform', function (d, i) { - return 'translate(' + (width + 2 + 0.5 * gridFieldWidth) + ',0)' + 'translate(0,' + - (i * gridFieldHeight + 0.5 * gridFieldHeight) + ')rotate(-90)'; - }) - .attr('dy', '0.35em') - .attr('text-anchor', 'middle') - .text('↑↓'); - - rowSortText.transition() - .duration(animationDuration) - .attr('transform', function (d, i) { - return 'translate(' + (width + 2 + 0.5 * gridFieldWidth) + ',0)' + 'translate(0,' + - (i * gridFieldHeight + 0.5 * gridFieldHeight) + ')rotate(-90)'; - }); + colSortBox.transition() + .duration(animationDuration) + .attr('x', function (d, i) { + return i * gridFieldWidth; + }) + .attr('y', -2 - gridFieldHeight) + .attr('width', gridFieldWidth) + .attr('height', gridFieldHeight); + + var rowSortText = rowSortItems.selectAll('.rowSortText') + .data(uids, function (d) { + return d; + }); - var rowSortBox = rowSortItems.selectAll('.rowSortBox') - .data(uids, function (d) { - return d; - }); + rowSortText.enter() + .append('text') + .attr('class', 'text rowSortText') + .attr('transform', function (d, i) { + return 'translate(' + (width + 2 + 0.5 * gridFieldWidth) + ',0)' + 'translate(0,' + + (i * gridFieldHeight + 0.5 * gridFieldHeight) + ')rotate(-90)'; + }) + .attr('dy', '0.35em') + .attr('text-anchor', 'middle') + .text('↑↓'); + + rowSortText.transition() + .duration(animationDuration) + .attr('transform', function (d, i) { + return 'translate(' + (width + 2 + 0.5 * gridFieldWidth) + ',0)' + 'translate(0,' + + (i * gridFieldHeight + 0.5 * gridFieldHeight) + ')rotate(-90)'; + }); - rowSortBox.enter() - .append('rect') - .attr('class', 'box rowSortBox') - .attr('x', width + 2) - .attr('y', function (d, i) { - return i * gridFieldHeight; - }) - .attr('width', gridFieldWidth) - .attr('height', gridFieldHeight) - .on('click', function (uid) { - d3.selectAll('.box').classed('sortedBy', false); - d3.select(this).classed('sortedBy', true); - - var colValues = patientIDs.map(function (patientID, idx) { - return [idx, getValueForSquareSorting(patientID, uid)]; + var rowSortBox = rowSortItems.selectAll('.rowSortBox') + .data(uids, function (d) { + return d; }); - if (isSorted(colValues)) { - colValues.sort(function (a, b) { - return a[1] - b[1]; + + rowSortBox.enter() + .append('rect') + .attr('class', 'box rowSortBox') + .attr('x', width + 2) + .attr('y', function (d, i) { + return i * gridFieldHeight; + }) + .attr('width', gridFieldWidth) + .attr('height', gridFieldHeight) + .on('click', function (uid) { + d3.selectAll('.box').classed('sortedBy', false); + d3.select(this).classed('sortedBy', true); + + var colValues = patientIDs.map(function (patientID, idx) { + return [idx, getValueForSquareSorting(patientID, uid)]; }); - } else { - colValues.sort(function (a, b) { - return b[1] - a[1]; + if (isSorted(colValues)) { + colValues.sort(function (a, b) { + return a[1] - b[1]; + }); + } else { + colValues.sort(function (a, b) { + return b[1] - a[1]; + }); + } + var sortValues = colValues.map(function (colValue) { + return colValue[0]; }); - } - var sortValues = colValues.map(function (colValue) { - return colValue[0]; + updateColOrder(sortValues); }); - updateColOrder(sortValues); - }); - - rowSortBox.transition() - .duration(animationDuration) - .attr('x', width + 2) - .attr('y', function (d, i) { - return i * gridFieldHeight; - }) - .attr('width', gridFieldWidth) - .attr('height', gridFieldHeight); - - var significanceSortText = significanceSortItems.selectAll('.significanceSortText') - .data(['something'], function (d) { - return d; - }); - significanceSortText.enter() - .append('text') - .attr('class', 'text significanceSortText') - .attr('x', -gridFieldWidth - 10 + 0.5 * gridFieldWidth) - .attr('y', -2 - gridFieldHeight + 0.5 * gridFieldHeight) - .attr('dy', '0.35em') - .attr('text-anchor', 'middle') - .text('↑↓'); - - significanceSortText.transition() - .duration(animationDuration) - .attr('x', -gridFieldWidth - 10 + 0.5 * gridFieldWidth) - .attr('y', -2 - gridFieldHeight + 0.5 * gridFieldHeight); - - var significanceSortBox = significanceSortItems.selectAll('.significanceSortBox') - .data(['something'], function (d) { - return d; - }); + rowSortBox.transition() + .duration(animationDuration) + .attr('x', width + 2) + .attr('y', function (d, i) { + return i * gridFieldHeight; + }) + .attr('width', gridFieldWidth) + .attr('height', gridFieldHeight); + + var significanceSortText = significanceSortItems.selectAll('.significanceSortText') + .data(['something'], function (d) { + return d; + }); - significanceSortBox.enter() - .append('rect') - .attr('class', 'box significanceSortBox') - .attr('x', -gridFieldWidth - 10) - .attr('y', -2 - gridFieldHeight) - .attr('width', gridFieldWidth) - .attr('height', gridFieldHeight) - .on('click', function () { - d3.selectAll('.box').classed('sortedBy', false); - d3.select(this).classed('sortedBy', true); - - var rowValues = significanceValues.map(function (significanceValue, idx) { - return [idx, getInternalSortValue(significanceValue)]; + significanceSortText.enter() + .append('text') + .attr('class', 'text significanceSortText') + .attr('x', -gridFieldWidth - 10 + 0.5 * gridFieldWidth) + .attr('y', -2 - gridFieldHeight + 0.5 * gridFieldHeight) + .attr('dy', '0.35em') + .attr('text-anchor', 'middle') + .text('↑↓'); + + significanceSortText.transition() + .duration(animationDuration) + .attr('x', -gridFieldWidth - 10 + 0.5 * gridFieldWidth) + .attr('y', -2 - gridFieldHeight + 0.5 * gridFieldHeight); + + var significanceSortBox = significanceSortItems.selectAll('.significanceSortBox') + .data(['something'], function (d) { + return d; }); - if (isSorted(rowValues)) { - rowValues.sort(function (a, b) { - return a[1] - b[1]; + significanceSortBox.enter() + .append('rect') + .attr('class', 'box significanceSortBox') + .attr('x', -gridFieldWidth - 10) + .attr('y', -2 - gridFieldHeight) + .attr('width', gridFieldWidth) + .attr('height', gridFieldHeight) + .on('click', function () { + d3.selectAll('.box').classed('sortedBy', false); + d3.select(this).classed('sortedBy', true); + + var rowValues = significanceValues.map(function (significanceValue, idx) { + return [idx, getInternalSortValue(significanceValue)]; }); - } else { - rowValues.sort(function (a, b) { - return b[1] - a[1]; + + if (isSorted(rowValues)) { + rowValues.sort(function (a, b) { + return a[1] - b[1]; + }); + } else { + rowValues.sort(function (a, b) { + return b[1] - a[1]; + }); + } + var sortValues = rowValues.map(function (rowValue) { + return rowValue[0]; }); - } - var sortValues = rowValues.map(function (rowValue) { - return rowValue[0]; + updateRowOrder(sortValues); }); - updateRowOrder(sortValues); - }); - significanceSortBox.transition() - .duration(animationDuration) - .attr('x', -gridFieldWidth - 10) - .attr('y', -2 - gridFieldHeight) - .attr('width', gridFieldWidth) - .attr('height', gridFieldHeight); + significanceSortBox.transition() + .duration(animationDuration) + .attr('x', -gridFieldWidth - 10) + .attr('y', -2 - gridFieldHeight) + .attr('width', gridFieldWidth) + .attr('height', gridFieldHeight); - var selectText = selectItems.selectAll('.selectText') - .data(patientIDs, function (d) { - return d; - }); + var selectText = selectItems.selectAll('.selectText') + .data(patientIDs, function (d) { + return d; + }); - selectText.enter() - .append('text') - .attr('class', function (d) { - return 'text selectText patientID-' + smartRUtils.makeSafeForCSS(d); - }) - .attr('x', function (d, i) { - return i * gridFieldWidth + 0.5 * gridFieldWidth; - }) - .attr('y', -2 - gridFieldHeight * 2 + 0.5 * gridFieldHeight) - .attr('dy', '0.35em') - .attr('text-anchor', 'middle') - .text('□'); - - selectText.transition() - .duration(animationDuration) - .attr('x', function (d, i) { - return i * gridFieldWidth + 0.5 * gridFieldWidth; - }) - .attr('y', -2 - gridFieldHeight * 2 + 0.5 * gridFieldHeight); - - var selectBox = selectItems.selectAll('.selectBox') - .data(patientIDs, function (d) { - return d; - }); + selectText.enter() + .append('text') + .attr('class', function (d) { + return 'text selectText patientID-' + smartRUtils.makeSafeForCSS(d); + }) + .attr('x', function (d, i) { + return i * gridFieldWidth + 0.5 * gridFieldWidth; + }) + .attr('y', -2 - gridFieldHeight * 2 + 0.5 * gridFieldHeight) + .attr('dy', '0.35em') + .attr('text-anchor', 'middle') + .text('□'); - selectBox.enter() - .append('rect') - .attr('class', 'box selectBox') - .attr('x', function (d, i) { - return i * gridFieldWidth; - }) - .attr('y', -2 - gridFieldHeight * 2) - .attr('width', gridFieldWidth) - .attr('height', gridFieldHeight) - .on('click', function (patientID) { - selectCol(patientID); - }); + selectText.transition() + .duration(animationDuration) + .attr('x', function (d, i) { + return i * gridFieldWidth + 0.5 * gridFieldWidth; + }) + .attr('y', -2 - gridFieldHeight * 2 + 0.5 * gridFieldHeight); - selectBox.transition() - .duration(animationDuration) - .attr('x', function (d, i) { - return i * gridFieldWidth; - }) - .attr('y', -2 - gridFieldHeight * 2) - .attr('width', gridFieldWidth) - .attr('height', gridFieldHeight); - - var patientID = patientIDItems.selectAll('.patientID') - .data(patientIDs, function (d) { - return d; - }); + var selectBox = selectItems.selectAll('.selectBox') + .data(patientIDs, function (d) { + return d; + }); - patientID.enter() - .append('text') - .attr('class', function (d) { - return 'patientID patientID-' + smartRUtils.makeSafeForCSS(d); - }) - .attr('transform', function (d) { - return 'translate(' + (patientIDs.indexOf(d) * gridFieldWidth) + ',0)' + - 'translate(' + (gridFieldWidth / 2) + ',' + (-4 - gridFieldHeight * 2) + ')rotate(-45)'; - }) - .style('text-anchor', 'start') - .text(function (d) { - return d; - }); + selectBox.enter() + .append('rect') + .attr('class', 'box selectBox') + .attr('x', function (d, i) { + return i * gridFieldWidth; + }) + .attr('y', -2 - gridFieldHeight * 2) + .attr('width', gridFieldWidth) + .attr('height', gridFieldHeight) + .on('click', function (patientID) { + selectCol(patientID); + }); - patientID.transition() - .duration(animationDuration) - .attr('transform', function (d) { - return 'translate(' + (patientIDs.indexOf(d) * gridFieldWidth) + ',0)' + - 'translate(' + (gridFieldWidth / 2) + ',' + (-4 - gridFieldHeight * 2) + ')rotate(-45)'; - }); + selectBox.transition() + .duration(animationDuration) + .attr('x', function (d, i) { + return i * gridFieldWidth; + }) + .attr('y', -2 - gridFieldHeight * 2) + .attr('width', gridFieldWidth) + .attr('height', gridFieldHeight); + + var patientID = patientIDItems.selectAll('.patientID') + .data(patientIDs, function (d) { + return d; + }); - var uid = labelItems.selectAll('.uid') - .data(uids, function (d) { - return d; - }); + patientID.enter() + .append('text') + .attr('class', function (d) { + return 'patientID patientID-' + smartRUtils.makeSafeForCSS(d); + }) + .attr('transform', function (d) { + return 'translate(' + (patientIDs.indexOf(d) * gridFieldWidth) + ',0)' + + 'translate(' + (gridFieldWidth / 2) + ',' + (-4 - gridFieldHeight * 2) + ')rotate(-45)'; + }) + .style('text-anchor', 'start') + .text(function (d) { + return d; + }); - uid.enter() - .append('text') - .attr('class', function (d) { - return 'uid text uid-' + smartRUtils.makeSafeForCSS(d); - }) - .attr('x', width + gridFieldWidth + 7) - .attr('y', function (d) { - return uids.indexOf(d) * gridFieldHeight + 0.5 * gridFieldHeight; - }) - .attr('dy', '0.35em') - .style('text-anchor', 'start') - .text(function (d) { - return d; - }); + patientID.transition() + .duration(animationDuration) + .attr('transform', function (d) { + return 'translate(' + (patientIDs.indexOf(d) * gridFieldWidth) + ',0)' + + 'translate(' + (gridFieldWidth / 2) + ',' + (-4 - gridFieldHeight * 2) + ')rotate(-45)'; + }); - uid.transition() - .duration(animationDuration) - .attr('x', width + gridFieldWidth + 7) - .attr('y', function (d) { - return uids.indexOf(d) * gridFieldHeight + 0.5 * gridFieldHeight; - }); + var uid = labelItems.selectAll('.uid') + .data(uids, function (d) { + return d; + }); - var significanceIndexMap = $.map(significanceValues, function (d, i) { - return {significance: d, idx: i}; - }); + uid.enter() + .append('text') + .attr('class', function (d) { + return 'uid text uid-' + smartRUtils.makeSafeForCSS(d); + }) + .attr('x', width + gridFieldWidth + 7) + .attr('y', function (d) { + return uids.indexOf(d) * gridFieldHeight + 0.5 * gridFieldHeight; + }) + .attr('dy', '0.35em') + .style('text-anchor', 'start') + .text(function (d) { + return d; + }); - var bar = barItems.selectAll('.bar') - .data(significanceIndexMap, function (d) { - return d.idx; - }); + uid.transition() + .duration(animationDuration) + .attr('x', width + gridFieldWidth + 7) + .attr('y', function (d) { + return uids.indexOf(d) * gridFieldHeight + 0.5 * gridFieldHeight; + }); - bar.enter() - .append('rect') - .attr('class', function (d) { - return 'bar idx-' + smartRUtils.makeSafeForCSS(d.idx); - }) - .attr('width', function (d) { - return histogramScale(d.significance); - }) - .attr('height', gridFieldHeight) - .attr('x', function (d) { - return -histogramScale(d.significance); - }) - .attr('y', function (d, idx) { - return gridFieldHeight * idx; - }) - .style('fill', function (d) { - return d.significance > 0 ? 'steelblue' : '#990000'; - }) - .on('mouseover', function (d) { - var html = 'Ranking (' + ranking + '): ' + d.significance; - tooltip - .style('visibility', 'visible') - .html(html) - .style('left', smartRUtils.mouseX(root) + 'px') - .style('top', smartRUtils.mouseY(root) + 'px'); - d3.selectAll('.square.uid-' + smartRUtils.makeSafeForCSS(uids[d.idx])) - .classed('squareHighlighted', true); - d3.select('.uid.uid-' + smartRUtils.makeSafeForCSS(uids[d.idx])) - .classed('highlight', true); - }) - .on('mouseout', function () { - tooltip.style('visibility', 'hidden'); - d3.selectAll('.square').classed('squareHighlighted', false); - d3.selectAll('.uid').classed('highlight', false); + var significanceIndexMap = $.map(significanceValues, function (d, i) { + return {significance: d, idx: i}; }); - bar.transition() - .duration(animationDuration) - .attr('height', gridFieldHeight) - .attr('width', function (d) { - return histogramScale(d.significance); - }) - .attr('x', function (d) { - return -histogramScale(d.significance); - }) - .attr('y', function (d) { - return gridFieldHeight * d.idx; - }) - .style('fill', function (d) { - return d.significance > 0 ? 'steelblue' : '#990000'; - }); + var bar = barItems.selectAll('.bar') + .data(significanceIndexMap, function (d) { + return d.idx; + }); - var featurePosY = -gridFieldWidth * 2 - getMaxWidth(d3.selectAll('.patientID')) - - features.length * gridFieldWidth / 2 - 20; + bar.enter() + .append('rect') + .attr('class', function (d) { + return 'bar idx-' + smartRUtils.makeSafeForCSS(d.idx); + }) + .attr('width', function (d) { + return histogramScale(d.significance); + }) + .attr('height', gridFieldHeight) + .attr('x', function (d) { + return -histogramScale(d.significance); + }) + .attr('y', function (d, idx) { + return gridFieldHeight * idx; + }) + .style('fill', function (d) { + return d.significance > 0 ? 'steelblue' : '#990000'; + }) + .on('mouseover', function (d) { + var html = 'Ranking (' + ranking + '): ' + d.significance; + tooltip + .style('visibility', 'visible') + .html(html) + .style('left', smartRUtils.mouseX(root) + 'px') + .style('top', smartRUtils.mouseY(root) + 'px'); + d3.selectAll('.square.uid-' + smartRUtils.makeSafeForCSS(uids[d.idx])) + .classed('squareHighlighted', true); + d3.select('.uid.uid-' + smartRUtils.makeSafeForCSS(uids[d.idx])) + .classed('highlight', true); + }) + .on('mouseout', function () { + tooltip.style('visibility', 'hidden'); + d3.selectAll('.square').classed('squareHighlighted', false); + d3.selectAll('.uid').classed('highlight', false); + }); - var extraSquare = featureItems.selectAll('.extraSquare') - .data(extraFields, function (d) { - return 'patientID-' + smartRUtils.makeSafeForCSS(d.PATIENTID) + - '-feature-' + smartRUtils.makeSafeForCSS(d.FEATURE); - }); + bar.transition() + .duration(animationDuration) + .attr('height', gridFieldHeight) + .attr('width', function (d) { + return histogramScale(d.significance); + }) + .attr('x', function (d) { + return -histogramScale(d.significance); + }) + .attr('y', function (d) { + return gridFieldHeight * d.idx; + }) + .style('fill', function (d) { + return d.significance > 0 ? 'steelblue' : '#990000'; + }); - extraSquare.enter() - .append('rect') - .attr('class', function (d) { - return 'extraSquare patientID-' + smartRUtils.makeSafeForCSS(d.PATIENTID) + - ' feature-' + smartRUtils.makeSafeForCSS(d.FEATURE); - }) - .attr('x', function (d) { - return patientIDs.indexOf(d.PATIENTID) * gridFieldWidth; - }) - .attr('y', function (d) { - return featurePosY + features.indexOf(d.FEATURE) * gridFieldHeight / 2; - }) - .attr('width', gridFieldWidth) - .attr('height', gridFieldHeight / 2) - .attr('rx', 0) - .attr('ry', 0) - .style('fill', 'white') - .on('mouseover', function (d) { - d3.select('.patientID.patientID-' + smartRUtils.makeSafeForCSS(d.PATIENTID)) - .classed('highlight', true); - d3.select('.feature.feature-' + smartRUtils.makeSafeForCSS(d.FEATURE)) - .classed('highlight', true); - var html = ''; - for (var key in d) { - html += key + ': ' + d[key] + '
'; - } - tooltip - .style('visibility', 'visible') - .html(html) - .style('left', smartRUtils.mouseX(root) + 'px') - .style('top', smartRUtils.mouseY(root) + 'px'); - }) - .on('mouseout', function (d) { - d3.selectAll('.patientID').classed('highlight', false); - d3.selectAll('.feature').classed('highlight', false); - tooltip.style('visibility', 'hidden'); - }); + var featurePosY = -gridFieldWidth * 2 - smartRUtils.getMaxWidth(d3.selectAll('.patientID')) - + features.length * gridFieldWidth / 2 - 20; - extraSquare.transition() - .duration(animationDuration) - .attr('x', function (d) { - return patientIDs.indexOf(d.PATIENTID) * gridFieldWidth; - }) - .attr('y', function (d) { - return featurePosY + features.indexOf(d.FEATURE) * gridFieldHeight / 2; - }) - .attr('width', gridFieldWidth) - .attr('height', gridFieldHeight / 2); - - var feature = featureItems.selectAll('.feature') - .data(features, function (d) { - return d; - }); + var extraSquare = featureItems.selectAll('.extraSquare') + .data(extraFields, function (d) { + return 'patientID-' + smartRUtils.makeSafeForCSS(d.PATIENTID) + + '-feature-' + smartRUtils.makeSafeForCSS(d.FEATURE); + }); - feature.enter() - .append('text') - .attr('class', function (d) { - return 'feature text feature-' + smartRUtils.makeSafeForCSS(d); - }) - .attr('x', width + gridFieldWidth + 7) - .attr('y', function (d) { - return featurePosY + features.indexOf(d) * gridFieldHeight / 2 + gridFieldHeight / 4; - }) - .attr('dy', '0.35em') - .style('text-anchor', 'start') - .text(function (d) { - return d; - }); + extraSquare.enter() + .append('rect') + .attr('class', function (d) { + return 'extraSquare patientID-' + smartRUtils.makeSafeForCSS(d.PATIENTID) + + ' feature-' + smartRUtils.makeSafeForCSS(d.FEATURE); + }) + .attr('x', function (d) { + return patientIDs.indexOf(d.PATIENTID) * gridFieldWidth; + }) + .attr('y', function (d) { + return featurePosY + features.indexOf(d.FEATURE) * gridFieldHeight / 2; + }) + .attr('width', gridFieldWidth) + .attr('height', gridFieldHeight / 2) + .attr('rx', 0) + .attr('ry', 0) + .style('fill', 'white') + .on('mouseover', function (d) { + d3.select('.patientID.patientID-' + smartRUtils.makeSafeForCSS(d.PATIENTID)) + .classed('highlight', true); + d3.select('.feature.feature-' + smartRUtils.makeSafeForCSS(d.FEATURE)) + .classed('highlight', true); + var html = ''; + for (var key in d) { + if (d.hasOwnProperty(key)) { + html += key + ': ' + d[key] + '
'; + } + } + tooltip + .style('visibility', 'visible') + .html(html) + .style('left', smartRUtils.mouseX(root) + 'px') + .style('top', smartRUtils.mouseY(root) + 'px'); + }) + .on('mouseout', function () { + d3.selectAll('.patientID').classed('highlight', false); + d3.selectAll('.feature').classed('highlight', false); + tooltip.style('visibility', 'hidden'); + }); - feature.transition() - .duration(animationDuration) - .attr('x', width + gridFieldWidth + 7) - .attr('y', function (d) { - return featurePosY + features.indexOf(d) * gridFieldHeight / 2 + gridFieldHeight / 4; - }); + extraSquare.transition() + .duration(animationDuration) + .attr('x', function (d) { + return patientIDs.indexOf(d.PATIENTID) * gridFieldWidth; + }) + .attr('y', function (d) { + return featurePosY + features.indexOf(d.FEATURE) * gridFieldHeight / 2; + }) + .attr('width', gridFieldWidth) + .attr('height', gridFieldHeight / 2); + + var feature = featureItems.selectAll('.feature') + .data(features, function (d) { + return d; + }); - var featureSortText = featureItems.selectAll('.featureSortText') - .data(features, function (d) { - return d; - }); + feature.enter() + .append('text') + .attr('class', function (d) { + return 'feature text feature-' + smartRUtils.makeSafeForCSS(d); + }) + .attr('x', width + gridFieldWidth + 7) + .attr('y', function (d) { + return featurePosY + features.indexOf(d) * gridFieldHeight / 2 + gridFieldHeight / 4; + }) + .attr('dy', '0.35em') + .style('text-anchor', 'start') + .text(function (d) { + return d; + }); - featureSortText.enter() - .append('text') - .attr('class', 'text featureSortText') - .attr('transform', function (d) { - return 'translate(' + (width + 2 + 0.5 * gridFieldWidth) + ',0)' + 'translate(0,' + - (featurePosY + features.indexOf(d) * gridFieldHeight / 2 + gridFieldHeight / 4) + - ')rotate(-90)'; - }) - .attr('dy', '0.35em') - .attr('text-anchor', 'middle') - .text('↑↓'); - - featureSortText.transition() - .duration(animationDuration) - .attr('transform', function (d) { - return 'translate(' + (width + 2 + 0.5 * gridFieldWidth) + ',0)' + 'translate(0,' + - (featurePosY + features.indexOf(d) * gridFieldHeight / 2 + gridFieldHeight / 4) + - ')rotate(-90)'; - }); + feature.transition() + .duration(animationDuration) + .attr('x', width + gridFieldWidth + 7) + .attr('y', function (d) { + return featurePosY + features.indexOf(d) * gridFieldHeight / 2 + gridFieldHeight / 4; + }); - var featureSortBox = featureItems.selectAll('.featureSortBox') - .data(features, function (d) { - return d; - }); + var featureSortText = featureItems.selectAll('.featureSortText') + .data(features, function (d) { + return d; + }); + + featureSortText.enter() + .append('text') + .attr('class', 'text featureSortText') + .attr('transform', function (d) { + return 'translate(' + (width + 2 + 0.5 * gridFieldWidth) + ',0)' + 'translate(0,' + + (featurePosY + features.indexOf(d) * gridFieldHeight / 2 + gridFieldHeight / 4) + + ')rotate(-90)'; + }) + .attr('dy', '0.35em') + .attr('text-anchor', 'middle') + .text('↑↓'); + + featureSortText.transition() + .duration(animationDuration) + .attr('transform', function (d) { + return 'translate(' + (width + 2 + 0.5 * gridFieldWidth) + ',0)' + 'translate(0,' + + (featurePosY + features.indexOf(d) * gridFieldHeight / 2 + gridFieldHeight / 4) + + ')rotate(-90)'; + }); + + var featureSortBox = featureItems.selectAll('.featureSortBox') + .data(features, function (d) { + return d; + }); - featureSortBox.enter() - .append('rect') - .attr('class', 'box featureSortBox') - .attr('x', width + 2) - .attr('y', function (d) { - return featurePosY + features.indexOf(d) * gridFieldHeight / 2; - }) - .attr('width', gridFieldWidth) - .attr('height', gridFieldHeight / 2) - .on('click', function (feature) { - d3.selectAll('.box').classed('sortedBy', false); - d3.select(this).classed('sortedBy', true); - - var featureValues = []; - var missingValues = false; - for (var i = 0; i < patientIDs.length; i++) { - var patientID = patientIDs[i]; - var value = (-Math.pow(2, 32)).toString(); - try { - var square = d3.select('.extraSquare' + '.patientID-' + - smartRUtils.makeSafeForCSS(patientID) + '.feature-' + - smartRUtils.makeSafeForCSS(feature)); - value = square.property('__data__').VALUE; - } catch (err) { - missingValues = true; + featureSortBox.enter() + .append('rect') + .attr('class', 'box featureSortBox') + .attr('x', width + 2) + .attr('y', function (d) { + return featurePosY + features.indexOf(d) * gridFieldHeight / 2; + }) + .attr('width', gridFieldWidth) + .attr('height', gridFieldHeight / 2) + .on('click', function (feature) { + d3.selectAll('.box').classed('sortedBy', false); + d3.select(this).classed('sortedBy', true); + + var featureValues = []; + var missingValues = false; + for (var i = 0; i < patientIDs.length; i++) { + var patientID = patientIDs[i]; + var value = (-Math.pow(2, 32)).toString(); + try { + var square = d3.select('.extraSquare' + '.patientID-' + + smartRUtils.makeSafeForCSS(patientID) + '.feature-' + + smartRUtils.makeSafeForCSS(feature)); + value = square.property('__data__').VALUE; + } catch (err) { + missingValues = true; + } + featureValues.push([i, value]); } - featureValues.push([i, value]); - } - if (isSorted(featureValues)) { - featureValues.sort(function (a, b) { - var diff = a[1] - b[1]; - return isNaN(diff) ? a[1].localeCompare(b[1]) : diff; - }); - } else { - featureValues.sort(function (a, b) { - var diff = b[1] - a[1]; - return isNaN(diff) ? b[1].localeCompare(a[1]) : diff; - }); - } - var sortValues = []; - for (var i = 0; i < featureValues.length; i++) { - sortValues.push(featureValues[i][0]); - } - if (missingValues) { - alert('Feature is missing for one or more patients.\n' + - 'Every missing value will be set to lowest possible value for sorting;'); - } - updateColOrder(sortValues); - }); + if (isSorted(featureValues)) { + featureValues.sort(function (a, b) { + var diff = a[1] - b[1]; + return isNaN(diff) ? a[1].localeCompare(b[1]) : diff; + }); + } else { + featureValues.sort(function (a, b) { + var diff = b[1] - a[1]; + return isNaN(diff) ? b[1].localeCompare(a[1]) : diff; + }); + } + var sortValues = []; + for (i = 0; i < featureValues.length; i++) { + sortValues.push(featureValues[i][0]); + } + if (missingValues) { + alert('Feature is missing for one or more patients.\n' + + 'Every missing value will be set to lowest possible value for sorting;'); + } + updateColOrder(sortValues); + }); - featureSortBox.transition() - .duration(animationDuration) - .attr('x', width + 2) - .attr('y', function (d, i) { - return featurePosY + features.indexOf(d) * gridFieldHeight / 2; - }) - .attr('width', gridFieldWidth) - .attr('height', gridFieldHeight / 2); - } + featureSortBox.transition() + .duration(animationDuration) + .attr('x', width + 2) + .attr('y', function (d) { + return featurePosY + features.indexOf(d) * gridFieldHeight / 2; + }) + .attr('width', gridFieldWidth) + .attr('height', gridFieldHeight / 2); + } - function zoom(zoomLevel) { - zoomLevel /= 100; - d3.selectAll('.patientID') - .style('font-size', Math.ceil(14 * zoomLevel) + 'px'); - d3.selectAll('.selectText') - .style('font-size', Math.ceil(16 * zoomLevel) + 'px'); - d3.selectAll('.uid') - .style('font-size', Math.ceil(12 * zoomLevel) + 'px'); - d3.selectAll('.feature') - .style('font-size', Math.ceil(10 * zoomLevel) + 'px'); - d3.selectAll('.significanceSortText, .rowSortText, .colSortText') - .style('font-size', Math.ceil(14 * zoomLevel) + 'px'); - d3.selectAll('.featureSortText') - .style('font-size', Math.ceil(10 * zoomLevel) + 'px'); - gridFieldWidth = 20 * zoomLevel; - gridFieldHeight = 20 * zoomLevel; - width = gridFieldWidth * patientIDs.length; - height = gridFieldHeight * uids.length; - heatmap - .attr('width', width + margin.left + margin.right) - .attr('height', width + margin.top + margin.bottom); - var temp = animationDuration; - animationDuration = 0; - updateHeatmap(); - reloadDendrograms(); - animationDuration = temp; - adjustDimensions(); - } + function zoom(zoomLevel) { + zoomLevel /= 100; + d3.selectAll('.patientID') + .style('font-size', Math.ceil(14 * zoomLevel) + 'px'); + d3.selectAll('.selectText') + .style('font-size', Math.ceil(16 * zoomLevel) + 'px'); + d3.selectAll('.uid') + .style('font-size', Math.ceil(12 * zoomLevel) + 'px'); + d3.selectAll('.feature') + .style('font-size', Math.ceil(10 * zoomLevel) + 'px'); + d3.selectAll('.significanceSortText, .rowSortText, .colSortText') + .style('font-size', Math.ceil(14 * zoomLevel) + 'px'); + d3.selectAll('.featureSortText') + .style('font-size', Math.ceil(10 * zoomLevel) + 'px'); + gridFieldWidth = 20 * zoomLevel; + gridFieldHeight = 20 * zoomLevel; + width = gridFieldWidth * patientIDs.length; + height = gridFieldHeight * uids.length; + heatmap + .attr('width', width + margin.left + margin.right) + .attr('height', width + margin.top + margin.bottom); + var temp = animationDuration; + animationDuration = 0; + updateHeatmap(); + reloadDendrograms(); + animationDuration = temp; + adjustDimensions(); + } - var cutoffLevel = 0; - - function animateCutoff(cutoff) { - cutoff = Math.floor(cutoff); - cutoffLevel = cutoff; - d3.selectAll('.square') - .classed('cuttoffHighlight', false); - d3.selectAll('.bar') - .classed('cuttoffHighlight', false); - d3.selectAll('.bar') - .map(function (d) { - return [d.idx, histogramScale(d.significance)]; - }) // This line is a bit hacky - .sort(function (a, b) { - return a[1] - b[1]; - }) - .filter(function (d, i) { - return i < cutoff; - }) - .each(function (d) { - d3.select('.bar.idx-' + smartRUtils.makeSafeForCSS(d[0])) - .classed('cuttoffHighlight', true); - d3.selectAll('.square.uid-' + smartRUtils.makeSafeForCSS(uids[d[0]])) - .classed('cuttoffHighlight', true); - }); - } + var cutoffLevel = 0; - function cutoff() { - //HeatmapService.startScriptExecution({ - // taskType: 'run', - // arguments: params, - // onUltimateSuccess: HeatmapService.runAnalysisSuccess, - // onUltimateFailure: HeatmapService.runAnalysisFailed, - // phase: 'run', - // progressMessage: 'Calculating', - // successMessage: undefined - //}); - // TODO: Use ajax service to be provided by ajaxServices.js to re-compute analysis - // with new arguments (in this case filter for cut-off) - params.max_row = maxRows - cutoffLevel - 1; - $('run-button input').click(); - } + function animateCutoff(cutoff) { + cutoff = Math.floor(cutoff); + cutoffLevel = cutoff; + d3.selectAll('.square') + .classed('cuttoffHighlight', false); + d3.selectAll('.bar') + .classed('cuttoffHighlight', false); + d3.selectAll('.bar') + .map(function (d) { + return [d.idx, histogramScale(d.significance)]; + }) // This line is a bit hacky + .sort(function (a, b) { + return a[1] - b[1]; + }) + .filter(function (d, i) { + return i < cutoff; + }) + .each(function (d) { + d3.select('.bar.idx-' + smartRUtils.makeSafeForCSS(d[0])) + .classed('cuttoffHighlight', true); + d3.selectAll('.square.uid-' + smartRUtils.makeSafeForCSS(uids[d[0]])) + .classed('cuttoffHighlight', true); + }); + } - function reloadDendrograms() { - if (colDendrogramVisible) { - removeColDendrogram(); - createColDendrogram(); + function cutoff() { + //HeatmapService.startScriptExecution({ + // taskType: 'run', + // arguments: params, + // onUltimateSuccess: HeatmapService.runAnalysisSuccess, + // onUltimateFailure: HeatmapService.runAnalysisFailed, + // phase: 'run', + // progressMessage: 'Calculating', + // successMessage: undefined + //}); + // TODO: Use ajax service to be provided by ajaxServices.js to re-compute analysis + // with new arguments (in this case filter for cut-off) + params.max_row = maxRows - cutoffLevel - 1; + $('run-button input').click(); } - if (rowDendrogramVisible) { - removeRowDendrogram(); - createRowDendrogram(); + + function reloadDendrograms() { + if (colDendrogramVisible) { + removeColDendrogram(); + createColDendrogram(); + } + if (rowDendrogramVisible) { + removeRowDendrogram(); + createRowDendrogram(); + } } - } - function selectCol(patientID) { - var colSquares = d3.selectAll('.square.patientID-' + smartRUtils.makeSafeForCSS(patientID)); - if (colSquares.classed('selected')) { - var index = selectedPatientIDs.indexOf(patientID); - selectedPatientIDs.splice(index, 1); - colSquares - .classed('selected', false); - d3.select('.selectText.patientID-' + smartRUtils.makeSafeForCSS(patientID)) - .text('□'); - } else { - selectedPatientIDs.push(patientID); - colSquares.classed('selected', true); - d3.select('.selectText.patientID-' + smartRUtils.makeSafeForCSS(patientID)) - .text('■'); + function selectCol(patientID) { + var colSquares = d3.selectAll('.square.patientID-' + smartRUtils.makeSafeForCSS(patientID)); + if (colSquares.classed('selected')) { + var index = selectedPatientIDs.indexOf(patientID); + selectedPatientIDs.splice(index, 1); + colSquares + .classed('selected', false); + d3.select('.selectText.patientID-' + smartRUtils.makeSafeForCSS(patientID)) + .text('□'); + } else { + selectedPatientIDs.push(patientID); + colSquares.classed('selected', true); + d3.select('.selectText.patientID-' + smartRUtils.makeSafeForCSS(patientID)) + .text('■'); + } + if (selectedPatientIDs.length !== 0) { + d3.selectAll('.square:not(.selected)') + .attr('opacity', 0.4); + } else { + d3.selectAll('.square') + .attr('opacity', 1); + } } - if (selectedPatientIDs.length !== 0) { - d3.selectAll('.square:not(.selected)') - .attr('opacity', 0.4); - } else { + + var colorScale; + function updateColors(schema) { + var redGreenScale = d3.scale.quantile() + .domain([0, 1]) + .range(function() { + var colorSet = []; + var NUM = 100; + var i = NUM; + while (i--) { + colorSet.push(d3.rgb((255 * i) / NUM, 0, 0)); + } + i = NUM; + while (i--) { + colorSet.push(d3.rgb(0, (255 * (NUM - i)) / NUM, 0)); + } + return colorSet.reverse(); + }()); + + var redBlueScale = d3.scale.quantile() + .domain([0, 1]) + .range(function() { + var colorSet = []; + var NUM = 100; + var i = NUM; + while (i--) { + colorSet.push(d3.rgb((255 * i) / NUM, 0, 0)); + } + i = NUM; + while (i--) { + colorSet.push(d3.rgb(0, 0, (255 * (NUM - i)) / NUM)); + } + return colorSet.reverse(); + }()); + + var blueScale = d3.scale.linear() + .domain([0, 1]) + .range(['#0000ff', '#e5e5ff']); + + var greenScale = d3.scale.linear() + .domain([0, 1]) + .range(['#00ff00', '#e5ffe5']); + + var colorSchemas = { + redGreen: redGreenScale, + blueScale: blueScale, + redBlue: redBlueScale, + greenScale: greenScale + }; + + colorScale = colorSchemas[schema]; + d3.selectAll('.square') + .transition() + .duration(animationDuration) + .style('fill', function (d) { + return colorScale(1 / (1 + Math.pow(Math.E, -d.ZSCORE))); + }); + + var featureColorSetBinary = ['#FF8000', '#FFFF00']; + var featureColorSetSequential = [ + 'rgb(247,252,253)', 'rgb(224,236,244)', 'rgb(191,211,230)', + 'rgb(158,188,218)', 'rgb(140,150,198)', 'rgb(140,107,177)', + 'rgb(136,65,157)', 'rgb(129,15,124)', 'rgb(77,0,75)' + ]; + var featureColorCategorical = d3.scale.category10(); + + features.forEach(function(feature) { + d3.selectAll('.extraSquare.feature-' + smartRUtils.makeSafeForCSS(feature)) + .style('fill', function (d) { + switch (d.TYPE) { + case 'binary': + return featureColorSetBinary[d.VALUE]; + case 'subset': + return featureColorSetBinary[d.VALUE - 1]; + case 'numerical': + colorScale.range(featureColorSetSequential); + return colorScale(1 / (1 + Math.pow(Math.E, -d.ZSCORE))); + default: + return featureColorCategorical(d.VALUE); + } + }); + }); + + updateLegend(); + } + + function updateLegend() { + var legendElementWidth = legendWidth / uniqueSortedZscores.length; + var legendElementHeight = legendHeight; + + var legendColor = legendItems.selectAll('.legendColor') + .data(uniqueSortedZscores, function(d) { return d; }); + + legendColor.enter() + .append('rect') + .attr('class', 'legendColor') + .attr('x', function(d, i) { + return 2 - margin.left + buttonPadding + buttonWidth + i * legendElementWidth; + }) + .attr('y', 8 - margin.top + buttonHeight * 4 + buttonPadding * 4) + .attr('width', Math.ceil(legendElementWidth)) + .attr('height', legendElementHeight) + .style('fill', function(d) { return colorScale(1 / (1 + Math.pow(Math.E, -d))); }); + + legendColor.transition() + .duration(animationDuration) + .style('fill', function(d) { return colorScale(1 / (1 + Math.pow(Math.E, -d))); }); + + var legendText = legendItems.selectAll('.legendText') + .data(uniqueSortedZscores, function(d) { return d; }); + + legendText.enter() + .append('text') + .attr('class', 'legendText') + .attr('x', function(d, i) { + return 2 - margin.left + buttonPadding + buttonWidth + i * legendElementWidth; + }) + .attr('y', 8 - margin.top + buttonHeight * 4 + buttonPadding * 4 + legendHeight + 10) + .attr('text-anchor', 'middle') + .text(function(d, i) { + if (i === 0 || i === uniqueSortedZscores.length - 1) { + return Number((uniqueSortedZscores.min()).toFixed(1)); + } else { + return null; + } + }); + + legendText.transition() + .text(function(d, i) { + if (i === 0) { + return Number((uniqueSortedZscores.min()).toFixed(1)); + } else if (i === uniqueSortedZscores.length - 1) { + return Number((uniqueSortedZscores.max()).toFixed(1)); + } else { + return null; + } + }); + } + + function unselectAll() { + d3.selectAll('.selectText') + .text('□'); + d3.selectAll('.square') + .classed('selected', false) .attr('opacity', 1); + selectedPatientIDs = []; } - } - var colorScale; - function updateColors(schema) { - var redGreenScale = d3.scale.quantile() - .domain([0, 1]) - .range(function() { - var colorSet = []; - var NUM = 100; - var i = NUM; - while (i--) { - colorSet.push(d3.rgb((255 * i) / NUM, 0, 0)); - } - i = NUM; - while (i--) { - colorSet.push(d3.rgb(0, (255 * (NUM - i)) / NUM, 0)); - } - return colorSet.reverse(); - }()); - - var redBlueScale = d3.scale.quantile() - .domain([0, 1]) - .range(function() { - var colorSet = []; - var NUM = 100; - var i = NUM; - while (i--) { - colorSet.push(d3.rgb((255 * i) / NUM, 0, 0)); - } - i = NUM; - while (i--) { - colorSet.push(d3.rgb(0, 0, (255 * (NUM - i)) / NUM)); - } - return colorSet.reverse(); - }()); - - var blueScale = d3.scale.linear() - .domain([0, 1]) - .range(['#0000ff', '#e5e5ff']); - - var greenScale = d3.scale.linear() - .domain([0, 1]) - .range(['#00ff00', '#e5ffe5']); - - var colorSchemas = { - redGreen: redGreenScale, - blueScale: blueScale, - redBlue: redBlueScale, - greenScale: greenScale - }; + var colDendrogramVisible = false; + var colDendrogram; - colorScale = colorSchemas[schema]; + function createColDendrogram() { + var w = 200; + var colDendrogramWidth = gridFieldWidth * numberOfClusteredColumns; + var spacing = gridFieldWidth * 2 + smartRUtils.getMaxWidth(d3.selectAll('.patientID')) + + features.length * gridFieldHeight / 2 + 40; - d3.selectAll('.square') - .transition() - .duration(animationDuration) - .style('fill', function (d) { - return colorScale(1 / (1 + Math.pow(Math.E, -d.ZSCORE))); - }); + var cluster = d3.layout.cluster() + .size([colDendrogramWidth, w]) + .separation(function() { + return 1; + }); - var featureColorSetBinary = ['#FF8000', '#FFFF00']; - var featureColorSetSequential = [ - 'rgb(247,252,253)', 'rgb(224,236,244)', 'rgb(191,211,230)', - 'rgb(158,188,218)', 'rgb(140,150,198)', 'rgb(140,107,177)', - 'rgb(136,65,157)', 'rgb(129,15,124)', 'rgb(77,0,75)' - ]; - var featureColorCategorical = d3.scale.category10(); + var diagonal = d3.svg.diagonal() + .projection(function(d) { + return [d.x, -spacing - w + d.y]; + }); - features.forEach(function(feature) { - d3.selectAll('.extraSquare.feature-' + smartRUtils.makeSafeForCSS(feature)) - .style('fill', function (d) { - switch (d.TYPE) { - case 'binary': - return featureColorSetBinary[d.VALUE]; - case 'subset': - return featureColorSetBinary[d.VALUE - 1]; - case 'numerical': - colorScale.range(featureColorSetSequential); - return colorScale(1 / (1 + Math.pow(Math.E, -d.ZSCORE))); - default: - return featureColorCategorical(d.VALUE); + var colDendrogramNodes = cluster.nodes(colDendrogram); + var colDendrogramLinks = cluster.links(colDendrogramNodes); + + heatmap.selectAll('.colDendrogramLink') + .data(colDendrogramLinks) + .enter().append('path') + .attr('class', 'colDendrogram link') + .attr('d', diagonal); + + heatmap.selectAll('.colDendrogramNode') + .data(colDendrogramNodes) + .enter().append('circle') + .attr('class', 'colDendrogram node') + .attr('r', 4.5) + .attr('transform', function (d) { + return 'translate(' + d.x + ',' + (-spacing - w + d.y) + ')'; + }).on('click', function (d) { + var previousSelection = selectedPatientIDs.slice(); + unselectAll(); + var leafs = d.index.split(' '); + for (var i = 0; i < leafs.length; i++) { + var patientID = patientIDs[leafs[i]]; + selectCol(patientID); + } + if (previousSelection.sort().toString() === selectedPatientIDs.sort().toString()) { + unselectAll(); } + }) + .on('mouseover', function (d) { + tooltip + .style('visibility', 'visible') + .html('Height: ' + d.height) + .style('left', smartRUtils.mouseX(root) + 'px') + .style('top', smartRUtils.mouseY(root) + 'px'); + }) + .on('mouseout', function () { + tooltip.style('visibility', 'hidden'); }); - }); + colDendrogramVisible = true; + } - updateLegend(); - } + var rowDendrogramVisible = false; + var rowDendrogram; - function updateLegend() { - var legendElementWidth = legendWidth / uniqueSortedZscores.length; - var legendElementHeight = legendHeight; - - var legendColor = legendItems.selectAll('.legendColor') - .data(uniqueSortedZscores, function(d) { return d; }); - - legendColor.enter() - .append('rect') - .attr('class', 'legendColor') - .attr('x', function(d, i) { - return 2 - margin.left + buttonPadding * 1 + buttonWidth * 1 + i * legendElementWidth; - }) - .attr('y', 8 - margin.top + buttonHeight * 4 + buttonPadding * 4) - .attr('width', Math.ceil(legendElementWidth)) - .attr('height', legendElementHeight) - .style('fill', function(d) { return colorScale(1 / (1 + Math.pow(Math.E, -d))); }); - - legendColor.transition() - .duration(animationDuration) - .style('fill', function(d) { return colorScale(1 / (1 + Math.pow(Math.E, -d))); }); - - var legendText = legendItems.selectAll('.legendText') - .data(uniqueSortedZscores, function(d) { return d; }); - - legendText.enter() - .append('text') - .attr('class', 'legendText') - .attr('x', function(d, i) { - return 2 - margin.left + buttonPadding * 1 + buttonWidth * 1 + i * legendElementWidth; - }) - .attr('y', 8 - margin.top + buttonHeight * 4 + buttonPadding * 4 + legendHeight + 10) - .attr('text-anchor', 'middle') - .text(function(d, i) { - if (i === 0 || i === uniqueSortedZscores.length - 1) { - return Number((uniqueSortedZscores.min()).toFixed(1)); - } else { - return null; - } - }); + function createRowDendrogram() { + var h = 280; + var rowDendrogramHeight = gridFieldWidth * numberOfClusteredRows; + var spacing = gridFieldWidth + smartRUtils.getMaxWidth(d3.selectAll('.uid')) + 20; - legendText.transition() - .text(function(d, i) { - if (i === 0) { - return Number((uniqueSortedZscores.min()).toFixed(1)); - } else if (i === uniqueSortedZscores.length - 1) { - return Number((uniqueSortedZscores.max()).toFixed(1)); - } else { - return null; - } - }); - } + var cluster = d3.layout.cluster() + .size([rowDendrogramHeight, h]) + .separation(function () { + return 1; + }); - function unselectAll() { - d3.selectAll('.selectText') - .text('□'); - d3.selectAll('.square') - .classed('selected', false) - .attr('opacity', 1); - selectedPatientIDs = []; - } + var diagonal = d3.svg.diagonal() + .projection(function (d) { + return [width + spacing + h - d.y, d.x]; + }); - var colDendrogramVisible = false; - var colDendrogram; + var rowDendrogramNodes = cluster.nodes(rowDendrogram); + var rowDendrogramLinks = cluster.links(rowDendrogramNodes); + + heatmap.selectAll('.rowDendrogramLink') + .data(rowDendrogramLinks) + .enter().append('path') + .attr('class', 'rowDendrogram link') + .attr('d', diagonal); + + heatmap.selectAll('.rowDendrogramNode') + .data(rowDendrogramNodes) + .enter().append('circle') + .attr('class', 'rowDendrogram node') + .attr('r', 4.5) + .attr('transform', function (d) { + return 'translate(' + (width + spacing + h - d.y) + ',' + d.x + ')'; + }).on('click', function (d) { + var leafs = d.index.split(' '); + var genes = []; + leafs.each(function (leaf) { + var uid = uids[leaf]; + var split = uid.split("--"); + split.shift(); + split.each(function (gene) { + genes.push(gene); + }); + }); + $.ajax({ + url: 'http://biocompendium.embl.de/cgi-bin/biocompendium.cgi', + type: 'POST', + timeout: '5000', + async: false, + data: { + section: 'upload_gene_lists_general', + primary_org: 'Human', + background: 'whole_genome', + Category1: 'Human', + gene_list_1: 'gene_list_1', + SubCat1: 'hgnc_symbol', + attachment1: genes.join(' ') + } + }).done(function (serverAnswer) { + var sessionID = serverAnswer.match(/tmp_\d+/)[0]; + var url = 'http://biocompendium.embl.de/' + + 'cgi-bin/biocompendium.cgi?section=pathway&pos=0&background=whole_genome&session=' + + sessionID + '&list=gene_list_1__1&list_size=15&org=human'; + window.open(url); + }).fail(function () { + alert('An error occurred. Maybe the external resource is unavailable.'); + }); + }) + .on('mouseover', function (d) { + tooltip + .style('visibility', 'visible') + .html('Height: ' + d.height) + .style('left', smartRUtils.mouseX(root) + 'px') + .style('top', smartRUtils.mouseY(root) + 'px'); + }) + .on('mouseout', function () { + tooltip.style('visibility', 'hidden'); + }); + rowDendrogramVisible = true; + } - function createColDendrogram() { - var w = 200; - var colDendrogramWidth = gridFieldWidth * numberOfClusteredColumns; - var spacing = gridFieldWidth * 2 + getMaxWidth(d3.selectAll('.patientID')) + - features.length * gridFieldHeight / 2 + 40; + function removeColDendrogram() { + heatmap.selectAll('.colDendrogram').remove(); + colDendrogramVisible = false; + } - var cluster = d3.layout.cluster() - .size([colDendrogramWidth, w]) - .separation(function (a, b) { - return 1; - }); + function removeRowDendrogram() { + heatmap.selectAll('.rowDendrogram').remove(); + rowDendrogramVisible = false; + } - var diagonal = d3.svg.diagonal() - .projection(function (d) { - return [d.x, -spacing - w + d.y]; + function updateColOrder(sortValues) { + patientIDs = sortValues.map(function (sortValue) { + return patientIDs[sortValue]; }); + unselectAll(); + removeColDendrogram(); + updateHeatmap(); + } - var colDendrogramNodes = cluster.nodes(colDendrogram); - var colDendrogramLinks = cluster.links(colDendrogramNodes); - - heatmap.selectAll('.colDendrogramLink') - .data(colDendrogramLinks) - .enter().append('path') - .attr('class', 'colDendrogram link') - .attr('d', diagonal); - - heatmap.selectAll('.colDendrogramNode') - .data(colDendrogramNodes) - .enter().append('circle') - .attr('class', 'colDendrogram node') - .attr('r', 4.5) - .attr('transform', function (d) { - return 'translate(' + d.x + ',' + (-spacing - w + d.y) + ')'; - }).on('click', function (d) { - var previousSelection = selectedPatientIDs.slice(); - unselectAll(); - var leafs = d.index.split(' '); - for (var i = 0; i < leafs.length; i++) { - var patientID = patientIDs[leafs[i]]; - selectCol(patientID); - } - if (arrEqual(previousSelection, selectedPatientIDs)) { - unselectAll(); - } - }) - .on('mouseover', function (d) { - tooltip - .style('visibility', 'visible') - .html('Height: ' + d.height) - .style('left', smartRUtils.mouseX(root) + 'px') - .style('top', smartRUtils.mouseY(root) + 'px'); - }) - .on('mouseout', function () { - tooltip.style('visibility', 'hidden'); + function updateRowOrder(sortValues) { + var sortedUIDs = []; + var sortedSignificanceValues = []; + var sortedLogfoldValues = []; + var sortedTtestValues = []; + var sortedPvalValues = []; + var sortedAdjpvalValues = []; + var sortedBvalValues = []; + + sortValues.each(function (sortValue) { + sortedUIDs.push(uids[sortValue]); + sortedSignificanceValues.push(significanceValues[sortValue]); + sortedLogfoldValues.push(logfoldValues[sortValue]); + sortedTtestValues.push(ttestValues[sortValue]); + sortedPvalValues.push(pvalValues[sortValue]); + sortedAdjpvalValues.push(adjpvalValues[sortValue]); + sortedBvalValues.push(bvalValues[sortValue]); }); - colDendrogramVisible = true; - } - - var rowDendrogramVisible = false; - var rowDendrogram; + uids = sortedUIDs; + significanceValues = sortedSignificanceValues; + logfoldValues = sortedLogfoldValues; + ttestValues = sortedTtestValues; + pvalValues = sortedPvalValues; + adjpvalValues = sortedAdjpvalValues; + bvalValues = sortedBvalValues; - function createRowDendrogram() { - var h = 280; - var rowDendrogramHeight = gridFieldWidth * numberOfClusteredRows; - var spacing = gridFieldWidth + getMaxWidth(d3.selectAll('.uid')) + 20; + removeRowDendrogram(); + updateHeatmap(); + animateCutoff(); + } - var cluster = d3.layout.cluster() - .size([rowDendrogramHeight, h]) - .separation(function () { - return 1; + function transformClusterOrderWRTInitialOrder(clusterOrder, initialOrder) { + return clusterOrder.map(function (d) { + return initialOrder.indexOf(d); }); + } - var diagonal = d3.svg.diagonal() - .projection(function (d) { - return [width + spacing + h - d.y, d.x]; + function getInitialRowOrder() { + return uids.map(function (uid) { + return originalUIDs.indexOf(uid); }); + } - var rowDendrogramNodes = cluster.nodes(rowDendrogram); - var rowDendrogramLinks = cluster.links(rowDendrogramNodes); - - heatmap.selectAll('.rowDendrogramLink') - .data(rowDendrogramLinks) - .enter().append('path') - .attr('class', 'rowDendrogram link') - .attr('d', diagonal); - - heatmap.selectAll('.rowDendrogramNode') - .data(rowDendrogramNodes) - .enter().append('circle') - .attr('class', 'rowDendrogram node') - .attr('r', 4.5) - .attr('transform', function (d) { - return 'translate(' + (width + spacing + h - d.y) + ',' + d.x + ')'; - }).on('click', function (d) { - var leafs = d.index.split(' '); - var genes = []; - leafs.each(function (leaf) { - var uid = uids[leaf]; - var split = uid.split("--"); - split.shift(); - split.each(function (gene) { - genes.push(gene); - }); - }); - $.ajax({ - url: 'http://biocompendium.embl.de/cgi-bin/biocompendium.cgi', - type: 'POST', - timeout: '5000', - async: false, - data: { - section: 'upload_gene_lists_general', - primary_org: 'Human', - background: 'whole_genome', - Category1: 'Human', - gene_list_1: 'gene_list_1', - SubCat1: 'hgnc_symbol', - attachment1: genes.join(' ') - } - }).done(function (serverAnswer) { - var sessionID = serverAnswer.match(/tmp_\d+/)[0]; - var url = 'http://biocompendium.embl.de/' + - 'cgi-bin/biocompendium.cgi?section=pathway&pos=0&background=whole_genome&session=' + - sessionID + '&list=gene_list_1__1&list_size=15&org=human'; - window.open(url); - }).fail(function () { - alert('An error occurred. Maybe the external resource is unavailable.'); - }); - }) - .on('mouseover', function (d) { - tooltip - .style('visibility', 'visible') - .html('Height: ' + d.height) - .style('left', smartRUtils.mouseX(root) + 'px') - .style('top', smartRUtils.mouseY(root) + 'px'); - }) - .on('mouseout', function () { - tooltip.style('visibility', 'hidden'); + function getInitialColOrder() { + return patientIDs.map(function (patientID) { + return originalPatientIDs.indexOf(patientID); }); - rowDendrogramVisible = true; - } + } - function removeColDendrogram() { - heatmap.selectAll('.colDendrogram').remove(); - colDendrogramVisible = false; - } + var lastUsedClustering = null; - function removeRowDendrogram() { - heatmap.selectAll('.rowDendrogram').remove(); - rowDendrogramVisible = false; - } + function cluster(clustering) { + if (!lastUsedClustering && typeof clustering === 'undefined') { + return; // Nothing should be done if clustering switches are turned on without clustering type set. + } + d3.selectAll('.box').classed('sortedBy', false); + clustering = (typeof clustering === 'undefined') ? lastUsedClustering : clustering; + var clusterData = data[clustering]; + if (rowClustering && numberOfClusteredRows > 0) { + rowDendrogram = JSON.parse(clusterData[3]); + updateRowOrder(transformClusterOrderWRTInitialOrder(clusterData[1], getInitialRowOrder())); + createRowDendrogram(rowDendrogram); + } else { + removeRowDendrogram(); + } + if (colClustering && numberOfClusteredColumns > 0) { + colDendrogram = JSON.parse(clusterData[2]); + updateColOrder(transformClusterOrderWRTInitialOrder(clusterData[0], getInitialColOrder())); + createColDendrogram(colDendrogram); + } else { + removeColDendrogram(); + } + lastUsedClustering = clustering; + } - function updateColOrder(sortValues) { - patientIDs = sortValues.map(function (sortValue) { - return patientIDs[sortValue]; - }); - unselectAll(); - removeColDendrogram(); - updateHeatmap(); - } + function switchRowClustering() { + rowClustering = !rowClustering; + cluster(); + } - function updateRowOrder(sortValues) { - var sortedUIDs = []; - var sortedSignificanceValues = []; - var sortedLogfoldValues = []; - var sortedTtestValues = []; - var sortedPvalValues = []; - var sortedAdjpvalValues = []; - var sortedBvalValues = []; - - sortValues.each(function (sortValue) { - sortedUIDs.push(uids[sortValue]); - sortedSignificanceValues.push(significanceValues[sortValue]); - sortedLogfoldValues.push(logfoldValues[sortValue]); - sortedTtestValues.push(ttestValues[sortValue]); - sortedPvalValues.push(pvalValues[sortValue]); - sortedAdjpvalValues.push(adjpvalValues[sortValue]); - sortedBvalValues.push(bvalValues[sortValue]); - }); - uids = sortedUIDs; - significanceValues = sortedSignificanceValues; - logfoldValues = sortedLogfoldValues; - ttestValues = sortedTtestValues; - pvalValues = sortedPvalValues; - adjpvalValues = sortedAdjpvalValues; - bvalValues = sortedBvalValues; - - removeRowDendrogram(); - updateHeatmap(); - animateCutoff(); - } + function switchColClustering() { + colClustering = !colClustering; + cluster(); + } - function transformClusterOrderWRTInitialOrder(clusterOrder, initialOrder) { - return clusterOrder.map(function (d) { - return initialOrder.indexOf(d); - }); - } + function changeRanking(method) { + ranking = method; + switch(method) { + case 'pval': + significanceValues = pvalValues; + break; + case 'adjpval': + significanceValues = adjpvalValues; + break; + case 'ttest': + significanceValues = ttestValues; + break; + case 'logfold': + significanceValues = logfoldValues; + break; + default: + significanceValues = bvalValues; + } + setScales(); + updateHeatmap(); + } - function getInitialRowOrder() { - return uids.map(function (uid) { - return originalUIDs.indexOf(uid); + function init() { + updateHeatmap(); + reloadDendrograms(); + updateColors('redGreen'); + } + + init(); + + controlElements.createD3Switch({ + location: heatmap, + onlabel: 'Animation ON', + offlabel: 'Animation OFF', + x: 2 - margin.left, + y: 8 - margin.top, + width: buttonWidth, + height: buttonHeight, + callback: switchAnimation, + checked: true }); - } - function getInitialColOrder() { - return patientIDs.map(function (patientID) { - return originalPatientIDs.indexOf(patientID); + controlElements.createD3Slider({ + location: heatmap, + label: 'Zoom in %', + x: 2 - margin.left + buttonPadding + buttonWidth, + y: 8 - margin.top - 10, + width: buttonWidth, + height: buttonHeight, + min: 1, + max: 200, + init: 100, + callback: zoom, + trigger: 'dragend' }); - } - var lastUsedClustering = null; + var cuttoffButton = controlElements.createD3Button({ + location: heatmap, + label: 'Apply Cutoff', + x: 2 - margin.left, + y: 8 - margin.top + buttonHeight + buttonPadding, + width: buttonWidth, + height: buttonHeight, + callback: cutoff + }); - function cluster(clustering) { - if (!lastUsedClustering && typeof clustering === 'undefined') { - return; // Nothing should be done if clustering switches are turned on without clustering type set. - } - d3.selectAll('.box').classed('sortedBy', false); - clustering = (typeof clustering === 'undefined') ? lastUsedClustering : clustering; - var clusterData = data[clustering]; - if (rowClustering && numberOfClusteredRows > 0) { - rowDendrogram = JSON.parse(clusterData[3]); - updateRowOrder(transformClusterOrderWRTInitialOrder(clusterData[1], getInitialRowOrder())); - createRowDendrogram(rowDendrogram); - } else { - removeRowDendrogram(); - } - if (colClustering && numberOfClusteredColumns > 0) { - colDendrogram = JSON.parse(clusterData[2]); - updateColOrder(transformClusterOrderWRTInitialOrder(clusterData[0], getInitialColOrder())); - createColDendrogram(colDendrogram); - } else { - removeColDendrogram(); - } - lastUsedClustering = clustering; - } + controlElements.createD3Slider({ + location: heatmap, + label: 'Cutoff', + x: 2 - margin.left + buttonPadding + buttonWidth, + y: 8 - margin.top + buttonHeight + buttonPadding - 10, + width: buttonWidth, + height: buttonHeight, + min: 0, + max: maxRows - 2, + init: 0, + callback: animateCutoff, + trigger: 'dragend' + }); - function switchRowClustering() { - rowClustering = !rowClustering; - cluster(); - } + controlElements.createD3Switch({ + location: heatmap, + onlabel: 'Clustering rows ON', + offlabel: 'Clustering rows OFF', + x: 2 - margin.left, + y: 8 - margin.top + buttonHeight * 3 + buttonPadding * 3, + width: buttonWidth, + height: buttonHeight, + callback: switchRowClustering, + checked: rowClustering + }); - function switchColClustering() { - colClustering = !colClustering; - cluster(); - } + controlElements.createD3Switch({ + location: heatmap, + onlabel: 'Clustering columns ON', + offlabel: 'Clustering columns OFF', + x: 2 - margin.left + buttonPadding + buttonWidth, + y: 8 - margin.top + buttonHeight * 3 + buttonPadding * 3, + width: buttonWidth, + height: buttonHeight, + callback: switchColClustering, + checked: colClustering + }); - function changeRanking(method) { - ranking = method; - switch(method) { - case 'pval': - significanceValues = pvalValues; - break; - case 'adjpval': - significanceValues = adjpvalValues; - break; - case 'ttest': - significanceValues = ttestValues; - break; - case 'logfold': - significanceValues = logfoldValues; - break; - default: - significanceValues = bvalValues; + if (ranking === 'bval' || + ranking === 'pval' || + ranking === 'adjpval' || + ranking === 'logfold' || + ranking === 'ttest') { + controlElements.createD3Dropdown({ + location: heatmap, + label: 'Ranking Method', + x: 2 - margin.left, + y: 8 - margin.top + buttonHeight * 4 + buttonPadding * 4, + width: buttonWidth, + height: buttonHeight, + items: [ + { + callback: function () { + changeRanking('bval'); + }, + label: 'B Value' + }, + { + callback: function () { + changeRanking('ttest'); + }, + label: 'T Test' + }, + { + callback: function () { + changeRanking('logfold'); + }, + label: 'Logfold' + }, + { + callback: function () { + changeRanking('pval'); + }, + label: 'p Value' + }, + { + callback: function () { + changeRanking('adjpval'); + }, + label: 'adj. p Value' + } + ] + }); } - setScales(); - updateHeatmap(); - } - function init() { - updateHeatmap(); - reloadDendrograms(); - updateColors('redGreen'); - } - init(); - - createD3Switch({ - location: heatmap, - onlabel: 'Animation ON', - offlabel: 'Animation OFF', - x: 2 - margin.left + buttonPadding * 0 + buttonWidth * 0, - y: 8 - margin.top + buttonHeight * 0 + buttonPadding * 0, - width: buttonWidth, - height: buttonHeight, - callback: switchAnimation, - checked: true - }); - - createD3Slider({ - location: heatmap, - label: 'Zoom in %', - x: 2 - margin.left + buttonPadding * 1 + buttonWidth * 1, - y: 8 - margin.top + buttonHeight * 0 + buttonPadding * 0 - 10, - width: buttonWidth, - height: buttonHeight, - min: 1, - max: 200, - init: 100, - callback: zoom, - trigger: 'dragend' - }); - - var cuttoffButton = createD3Button({ - location: heatmap, - label: 'Apply Cutoff', - x: 2 - margin.left + buttonPadding * 0 + buttonWidth * 0, - y: 8 - margin.top + buttonHeight * 1 + buttonPadding * 1, - width: buttonWidth, - height: buttonHeight, - callback: cutoff - }); - - createD3Slider({ - location: heatmap, - label: 'Cutoff', - x: 2 - margin.left + buttonPadding * 1 + buttonWidth * 1, - y: 8 - margin.top + buttonHeight * 1 + buttonPadding * 1 - 10, - width: buttonWidth, - height: buttonHeight, - min: 0, - max: maxRows - 2, - init: 0, - callback: animateCutoff, - trigger: 'dragend' - }); - - createD3Switch({ - location: heatmap, - onlabel: 'Clustering rows ON', - offlabel: 'Clustering rows OFF', - x: 2 - margin.left + buttonPadding * 0 + buttonWidth * 0, - y: 8 - margin.top + buttonHeight * 3 + buttonPadding * 3, - width: buttonWidth, - height: buttonHeight, - callback: switchRowClustering, - checked: rowClustering - }); - - createD3Switch({ - location: heatmap, - onlabel: 'Clustering columns ON', - offlabel: 'Clustering columns OFF', - x: 2 - margin.left + buttonPadding * 1 + buttonWidth * 1, - y: 8 - margin.top + buttonHeight * 3 + buttonPadding * 3, - width: buttonWidth, - height: buttonHeight, - callback: switchColClustering, - checked: colClustering - }); - - if (ranking === 'bval' || - ranking === 'pval' || - ranking === 'adjpval' || - ranking === 'logfold' || - ranking === 'ttest') { - createD3Dropdown({ + controlElements.createD3Dropdown({ location: heatmap, - label: 'Ranking Method', - x: 2 - margin.left + buttonPadding * 0 + buttonWidth * 0, - y: 8 - margin.top + buttonHeight * 4 + buttonPadding * 4, + label: 'Heatmap Coloring', + x: 2 - margin.left, + y: 8 - margin.top + buttonHeight * 2 + buttonPadding * 2, width: buttonWidth, height: buttonHeight, items: [ { callback: function () { - changeRanking('bval'); + updateColors('redGreen'); }, - label: 'B Value' + label: 'Red to Green Schema' }, { callback: function () { - changeRanking('ttest'); + updateColors('redBlue'); }, - label: 'T Test' + label: 'Red to Blue Schema' }, { callback: function () { - changeRanking('logfold'); + updateColors('blueScale'); }, - label: 'Logfold' + label: 'Blue Schema' }, { callback: function () { - changeRanking('pval'); + updateColors('greenScale'); }, - label: 'p Value' - }, - { - callback: function () { - changeRanking('adjpval'); - }, - label: 'adj. p Value' + label: 'Green Schema' } ] }); - } - - createD3Dropdown({ - location: heatmap, - label: 'Heatmap Coloring', - x: 2 - margin.left + buttonPadding * 0 + buttonWidth * 0, - y: 8 - margin.top + buttonHeight * 2 + buttonPadding * 2, - width: buttonWidth, - height: buttonHeight, - items: [ - { - callback: function () { - updateColors('redGreen'); - }, - label: 'Red to Green Schema' - }, - { - callback: function () { - updateColors('redBlue'); - }, - label: 'Red to Blue Schema' - }, - { - callback: function () { - updateColors('blueScale'); - }, - label: 'Blue Schema' - }, - { - callback: function () { - updateColors('greenScale'); - }, - label: 'Green Schema' - } - ] - }); - - createD3Dropdown({ - location: heatmap, - label: 'Heatmap Clustering', - x: 2 - margin.left + buttonPadding * 1 + buttonWidth * 1, - y: 8 - margin.top + buttonHeight * 2 + buttonPadding * 2, - width: buttonWidth, - height: buttonHeight, - items: [ - { - callback: function () { - cluster('hclustEuclideanAverage'); - }, - label: 'Hierarch.-Eucl.-Average' - }, - { - callback: function () { - cluster('hclustEuclideanComplete'); + controlElements.createD3Dropdown({ + location: heatmap, + label: 'Heatmap Clustering', + x: 2 - margin.left + buttonPadding + buttonWidth, + y: 8 - margin.top + buttonHeight * 2 + buttonPadding * 2, + width: buttonWidth, + height: buttonHeight, + items: [ + { + callback: function () { + cluster('hclustEuclideanAverage'); + }, + label: 'Hierarch.-Eucl.-Average' }, - label: 'Hierarch.-Eucl.-Complete' - }, - { - callback: function () { - cluster('hclustEuclideanSingle'); + { + callback: function () { + cluster('hclustEuclideanComplete'); + }, + label: 'Hierarch.-Eucl.-Complete' }, - label: 'Hierarch.-Eucl.-Single' - }, - { - callback: function () { - cluster('hclustManhattanAverage'); + { + callback: function () { + cluster('hclustEuclideanSingle'); + }, + label: 'Hierarch.-Eucl.-Single' }, - label: 'Hierarch.-Manhat.-Average' - }, - { - callback: function () { - cluster('hclustManhattanComplete'); + { + callback: function () { + cluster('hclustManhattanAverage'); + }, + label: 'Hierarch.-Manhat.-Average' }, - label: 'Hierarch.-Manhat.-Complete' - }, - { - callback: function () { - cluster('hclustManhattanSingle'); + { + callback: function () { + cluster('hclustManhattanComplete'); + }, + label: 'Hierarch.-Manhat.-Complete' }, - label: 'Hierarch.-Manhat.-Single' - } - ] - }); - } + { + callback: function () { + cluster('hclustManhattanSingle'); + }, + label: 'Hierarch.-Manhat.-Single' + } + ] + }); + } -}]); + }]); diff --git a/web-app/js/smartR/smartR.js b/web-app/js/smartR/smartR.js index d8514b4..489fddb 100644 --- a/web-app/js/smartR/smartR.js +++ b/web-app/js/smartR/smartR.js @@ -1,372 +1,6 @@ -function createD3Button(args) { - var button = args.location.append('g') +'use strict'; - var box = button.append('rect') - .attr('x', args.x) - .attr('y', args.y) - .attr('rx', 3) - .attr('ry', 3) - .attr('width', args.width) - .attr('height', args.height) - .style('stroke-width', '1px') - .style('stroke', '#009ac9') - .style('fill', '#009ac9') - .style('cursor', 'pointer') - .on('mouseover', function() { - box.transition() - .duration(300) - .style('fill', '#ffffff') - text.transition() - .duration(300) - .style('fill', '#009ac9') - }) - .on('mouseout', function() { - box.transition() - .duration(300) - .style('fill', '#009ac9') - text.transition() - .duration(300) - .style('fill', '#ffffff') - }) - .on('click', function() { return args.callback() }) - - var text = button.append('text') - .attr('x', args.x + args.width / 2) - .attr('y', args.y + args.height / 2) - .attr('dy', '0.35em') - .style('pointer-events', 'none') - .style('text-anchor', 'middle') - .style('fill', '#ffffff') - .style('font-size', '14px') - .text(args.label) - - return button -} - -function createD3Switch(args) { - var switcher = args.location.append('g') - - var checked = args.checked - var color = checked ? 'green' : 'red' - - var box = switcher.append('rect') - .attr('x', args.x) - .attr('y', args.y) - .attr('rx', 3) - .attr('ry', 3) - .attr('width', args.width) - .attr('height', args.height) - .style('stroke-width', '1px') - .style('stroke', color) - .style('fill', color) - .style('cursor', 'pointer') - .on('click', function() { - if (color === 'green') { - box.transition() - .duration(300) - .style('stroke', 'red') - .style('fill', 'red') - color = 'red' - checked = false - } else { - box.transition() - .duration(300) - .style('stroke', 'green') - .style('fill', 'green') - color = 'green' - checked = true - } - text.text(checked ? args.onlabel : args.offlabel) - args.callback(checked) - }) - - var text = switcher.append('text') - .attr('x', args.x + args.width / 2) - .attr('y', args.y + args.height / 2) - .attr('dy', '0.35em') - .style('pointer-events', 'none') - .style('text-anchor', 'middle') - .style('fill', '#ffffff') - .style('font-size', '14px') - .text(checked ? args.onlabel : args.offlabel) - - return switcher -} - -function createD3Dropdown(args) { - function shrink() { - dropdown.selectAll('.itemBox') - .attr('y', args.y + args.height) - .style('visibility', 'hidden') - dropdown.selectAll('.itemText') - .attr('y', args.y + args.height + args.height / 2) - .style('visibility', 'hidden') - itemHovered = false - hovered = false - itemHovered = false - } - var dropdown = args.location.append('g') - - var hovered = false - var itemHovered = false - - var itemBox = dropdown.selectAll('.itemBox') - .data(args.items, function(item) { return item.label }) - - itemBox.enter() - .append('rect') - .attr('class', 'itemBox') - .attr('x', args.x) - .attr('y', args.y + args.height) - .attr('rx', 0) - .attr('ry', 0) - .attr('width', args.width) - .attr('height', args.height) - .style('cursor', 'pointer') - .style('stroke-width', '2px') - .style('stroke', '#ffffff') - .style('fill', '#E3E3E3') - .style('visibility', 'hidden') - .on('mouseover', function() { - itemHovered = true - d3.select(this) - .style('fill', '#009ac9') - }) - .on('mouseout', function() { - itemHovered = false - d3.select(this) - .style('fill', '#E3E3E3') - setTimeout(function() { - if (! hovered && ! itemHovered) { - shrink() - } - }, 50) - }) - .on('click', function(d) { d.callback() }) - - var itemText = dropdown.selectAll('.itemText') - .data(args.items, function(item) { return item.label }) - - itemText - .enter() - .append('text') - .attr('class', 'itemText') - .attr('x', args.x + args.width / 2) - .attr('y', args.y + args.height + args.height / 2) - .attr('dy', '0.35em') - .style('pointer-events', 'none') - .style('text-anchor', 'middle') - .style('fill', '#000000') - .style('font-size', '14px') - .style('visibility', 'hidden') - .text(function(d) { return d.label }) - - var box = dropdown.append('rect') - .attr('x', args.x) - .attr('y', args.y) - .attr('rx', 3) - .attr('ry', 3) - .attr('width', args.width) - .attr('height', args.height) - .style('stroke-width', '1px') - .style('stroke', '#009ac9') - .style('fill', '#009ac9') - .on('mouseover', function() { - if (hovered) { - return - } - dropdown.selectAll('.itemBox') - .transition() - .duration(300) - .style('visibility', 'visible') - .attr('y', function(d) { - var labels = args.items.map(function(item) { return item.label; }); - var idx = labels.indexOf(d.label); - return 2 + args.y + (idx + 1) * args.height - }) - - dropdown.selectAll('.itemText') - .transition() - .duration(300) - .style('visibility', 'visible') - .attr('y', function(d) { - var labels = args.items.map(function(item) { return item.label; }); - var idx = labels.indexOf(d.label); - return 2 + args.y + (idx + 1) * args.height + args.height / 2 - }) - - hovered = true - }) - .on('mouseout', function() { - hovered = false - setTimeout(function() { - if (! hovered && ! itemHovered) { - shrink() - } - }, 50) - setTimeout(function() { // first check is not enough if animation interrupts it - if (! hovered && ! itemHovered) { - shrink() - } - }, 350) - }) - - var text = dropdown.append('text') - .attr('class', 'buttonText') - .attr('x', args.x + args.width / 2) - .attr('y', args.y + args.height / 2) - .attr('dy', '0.35em') - .style('pointer-events', 'none') - .style('text-anchor', 'middle') - .style('fill', '#ffffff') - .style('font-size', '14px') - .text(args.label) - - return dropdown -} - -function createD3Slider(args) { - var slider = args.location.append('g') - - var lineGen = d3.svg.line() - .x(function(d) { return d.x }) - .y(function(d) { return d.y }) - .interpolate('linear') - - var lineData = [ - {x: args.x, y: args.y + args.height}, - {x: args.x, y: args.y + 0.75 * args.height}, - {x: args.x + args.width, y: args.y + 0.75 * args.height}, - {x: args.x + args.width, y: args.y + args.height} - ] - - var sliderScale = d3.scale.linear() - .domain([args.min, args.max]) - .range([args.x, args.x + args.width]) - - slider.append('path') - .attr('d', lineGen(lineData)) - .style('pointer-events', 'none') - .style('stroke', '#009ac9') - .style('stroke-width', '2px') - .style('shape-rendering', 'crispEdges') - .style('fill', 'none') - - slider.append('text') - .attr('x', args.x) - .attr('y', args.y + args.height + 10) - .attr('dy', '0.35em') - .style('pointer-events', 'none') - .style('text-anchor', 'middle') - .style('fill', '#000000') - .style('font-size', '9px') - .text(args.min) - - slider.append('text') - .attr('x', args.x + args.width) - .attr('y', args.y + args.height + 10) - .attr('dy', '0.35em') - .style('pointer-events', 'none') - .style('text-anchor', 'middle') - .style('fill', '#000000') - .style('font-size', '9px') - .text(args.max) - - slider.append('text') - .attr('x', args.x + args.width / 2) - .attr('y', args.y + args.height) - .attr('dy', '0.35em') - .style('pointer-events', 'none') - .style('text-anchor', 'middle') - .style('fill', '#000000') - .style('font-size', '14px') - .text(args.label) - - var currentValue = args.init - - function move() { - var xPos = d3.event.x - if (xPos < args.x) { - xPos = args.x - } else if (xPos > args.x + args.width) { - xPos = args.x + args.width - } - - currentValue = Number(sliderScale.invert(xPos)).toFixed(5) - - dragger - .attr('x', xPos - 20) - handle - .attr('cx', xPos) - pointer - .attr('x1', xPos) - .attr('x2', xPos) - value - .attr('x', xPos + 10) - .text(currentValue) - } - - var drag = d3.behavior.drag() - .on('drag', move) - .on(args.trigger, function() { args.callback(currentValue) }) - - var dragger = slider.append('rect') - .attr('x', sliderScale(args.init) - 20) - .attr('y', args.y) - .attr('width', 40) - .attr('height', args.height) - .style('opacity', 0) - .style('cursor', 'pointer') - .on('mouseover', function() { - handle - .style('fill', '#009ac9') - pointer - .style('stroke', '#009ac9') - }) - .on('mouseout', function() { - handle - .style('fill', '#000000') - pointer - .style('stroke', '#000000') - }) - .call(drag) - - var handle = slider.append('circle') - .attr('cx', sliderScale(args.init)) - .attr('cy', args.y + 10) - .attr('r', 6) - .style('pointer-events', 'none') - .style('fill', '#000000') - - var pointer = slider.append('line') - .attr('x1', sliderScale(args.init)) - .attr('y1', args.y + 10) - .attr('x2', sliderScale(args.init)) - .attr('y2', args.y + 0.75 * args.height) - .style('pointer-events', 'none') - .style('stroke', '#000000') - .style('stroke-width', '1px') - - var value = slider.append('text') - .attr('x', sliderScale(args.init) + 10) - .attr('y', args.y + 10) - .attr('dy', '0.35em') - .style('pointer-events', 'none') - .style('text-anchor', 'start') - .style('fill', '#000000') - .style('font-size', '10px') - .text(args.init) - - return slider -} - -function getMaxWidth(selection) { - return selection[0].map(function (d) { - return d.getBBox().width; - }).max() -} - -window.addSmartRPanel = function addSmartRPanel(parentPanel, config) { +window.addSmartRPanel = function addSmartRPanel(parentPanel) { var smartRPanel = new Ext.Panel({ id: 'smartRPanel', title: 'SmartR',