├── .gitattributes ├── app ├── .buildignore ├── .htaccess ├── favicon.ico ├── components │ ├── charts │ │ ├── core.js │ │ ├── chart-legend-chart.js │ │ ├── chart.css │ │ ├── force-controller.js │ │ ├── hive-chart.js │ │ └── force-chart.js │ ├── ui │ │ ├── panel.html │ │ ├── gene-list-template.html │ │ ├── neighbors-list-template.html │ │ ├── saveModalContent.html │ │ ├── main.html │ │ ├── panel-directives.js │ │ ├── pageslide.css │ │ ├── right-panel.html │ │ ├── main-directives.js │ │ ├── panel-controller.js │ │ ├── item.html │ │ ├── searchModalContent.html │ │ ├── main.css │ │ ├── bottom-panel.html │ │ └── main.js │ ├── routes.js │ ├── app.js │ ├── graph │ │ └── graph-factory.js │ └── data │ │ └── ligandReceptorData-service.js ├── data │ └── ontology.txt └── index.html ├── .bowerrc ├── .gitignore ├── .travis.yml ├── test ├── runner.html ├── spec │ ├── controllers │ │ └── main.js │ └── services │ │ └── ligandReceptorData.js └── .jshintrc ├── .editorconfig ├── .jshintrc ├── package.json ├── karma-e2e.conf.js ├── bower.json ├── karma.conf.js ├── README.md └── Gruntfile.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /app/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | DirectoryIndex index.html 3 | 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hypercubed/connectome/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .tmp 4 | .sass-cache 5 | app/bower_components 6 | backup 7 | data_bak 8 | .grunt 9 | todo.md -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.8' 4 | - '0.10' 5 | before_script: 6 | - 'npm install -g bower grunt-cli' 7 | - 'bower install' 8 | -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | End2end Test Runner 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/components/charts/core.js: -------------------------------------------------------------------------------- 1 | (function(root) { 2 | 'use strict'; 3 | 4 | var lrd3 = window.lrd3 || {}; 5 | 6 | lrd3.utils = lrd3.utils || {}; 7 | lrd3.charts = lrd3.charts || {}; 8 | lrd3.models = lrd3.models || {}; 9 | 10 | root.lrd3 = lrd3; 11 | 12 | })(window); 13 | -------------------------------------------------------------------------------- /app/components/ui/panel.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 |
8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "angular": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/components/ui/gene-list-template.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | {{$last ? '' : ','}} 10 | 11 | 12 | (+ {{list.length - limit}} more) 13 | 14 | -------------------------------------------------------------------------------- /app/components/ui/neighbors-list-template.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | {{$last ? '' : ','}} 11 | 12 | 13 | (+ {{list.length - limit}} more) 14 | 15 | -------------------------------------------------------------------------------- /test/spec/controllers/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: MainCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('lrSpaApp')); 7 | 8 | var MainCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | MainCtrl = $controller('MainCtrl', { 15 | $scope: scope, 16 | loadedData: { 17 | pairs: [1,2,3], 18 | cells: [], 19 | genes: [] 20 | } 21 | }); 22 | })); 23 | 24 | it('should attach loaded data', function () { 25 | expect(scope.data.pairs.length).toBe(3); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "jasmine": false, 33 | "spyOn": false 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /app/components/routes.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('lrSpaApp') 5 | 6 | .config(function($stateProvider, $urlRouterProvider) { 7 | 8 | $urlRouterProvider.otherwise('/hive'); 9 | 10 | $stateProvider 11 | .state('home', { 12 | //abastract: true, 13 | resolve: { loadedData: ['ligandReceptorData', function(ligandReceptorData) { // note: this injection gets ignored by ng-min 14 | return ligandReceptorData.load(); 15 | }]}, 16 | url: '/', 17 | controller: 'MainCtrl', 18 | templateUrl: 'components/ui/main.html' 19 | }) 20 | .state('home.force-graph', { 21 | url: '^/force' 22 | }) 23 | .state('home.hive-graph', { 24 | url: '^/hive' 25 | }) 26 | .state('reset', { 27 | url: '^/reset', 28 | controller: 'ResetCtrl' 29 | }); 30 | 31 | }); 32 | 33 | })(); 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lrspa", 3 | "version": "0.0.7", 4 | "dependencies": {}, 5 | "devDependencies": { 6 | "grunt": "~0.4.1", 7 | "grunt-autoprefixer": "~0.4.0", 8 | "grunt-bower-install": "~1.0.0", 9 | "grunt-concurrent": "~0.5.0", 10 | "grunt-contrib-clean": "~0.5.0", 11 | "grunt-contrib-concat": "~0.3.0", 12 | "grunt-contrib-connect": "~0.5.0", 13 | "grunt-contrib-copy": "~0.4.1", 14 | "grunt-contrib-cssmin": "~0.7.0", 15 | "grunt-contrib-htmlmin": "~0.1.3", 16 | "grunt-contrib-imagemin": "~0.3.0", 17 | "grunt-contrib-jshint": "~0.7.1", 18 | "grunt-contrib-uglify": "~0.2.0", 19 | "grunt-contrib-watch": "~0.5.2", 20 | "grunt-gh-pages": "^0.10.0", 21 | "grunt-google-cdn": "~0.2.0", 22 | "grunt-karma": "^0.8.2", 23 | "grunt-newer": "~0.6.1", 24 | "grunt-ngmin": "~0.0.2", 25 | "grunt-rev": "~0.1.0", 26 | "grunt-rsync": "^0.5.0", 27 | "grunt-svgmin": "~0.2.0", 28 | "grunt-usemin": "~2.0.0", 29 | "jshint-stylish": "~0.1.3", 30 | "karma": "^0.12.6", 31 | "karma-jasmine": "^0.1.5", 32 | "karma-mocha-reporter": "^0.3.1", 33 | "karma-ng-html2js-preprocessor": "^0.1.0", 34 | "karma-ng-scenario": "^0.1.0", 35 | "karma-phantomjs-launcher": "^0.1.4", 36 | "load-grunt-tasks": "~0.4.0", 37 | "phantomjs": "^1.9.7-15", 38 | "time-grunt": "~0.2.1" 39 | }, 40 | "engines": { 41 | "node": ">=0.10.0" 42 | }, 43 | "scripts": { 44 | "test": "grunt test" 45 | }, 46 | "license": "MIT", 47 | "repository": "Hypercubed/connectome" 48 | } 49 | -------------------------------------------------------------------------------- /karma-e2e.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | // testing framework to use (jasmine/mocha/qunit/...) 10 | frameworks: ['ng-scenario'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'test/e2e/**/*.js' 15 | ], 16 | 17 | // list of files / patterns to exclude 18 | exclude: [], 19 | 20 | // web server port 21 | port: 8080, 22 | 23 | // level of logging 24 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 25 | logLevel: config.LOG_INFO, 26 | 27 | 28 | // enable / disable watching file and executing tests whenever any file changes 29 | autoWatch: false, 30 | 31 | 32 | // Start these browsers, currently available: 33 | // - Chrome 34 | // - ChromeCanary 35 | // - Firefox 36 | // - Opera 37 | // - Safari (only Mac) 38 | // - PhantomJS 39 | // - IE (only Windows) 40 | browsers: ['Chrome'], 41 | 42 | 43 | // Continuous Integration mode 44 | // if true, it capture browsers, run tests and exit 45 | singleRun: false 46 | 47 | // Uncomment the following lines if you are using grunt's server to run the tests 48 | // proxies: { 49 | // '/': 'http://localhost:9000/' 50 | // }, 51 | // URL root prevent conflicts with the site root 52 | // urlRoot: '_karma_' 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /app/components/ui/saveModalContent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 38 | 41 | -------------------------------------------------------------------------------- /app/components/ui/main.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 |
9 | 10 | 11 | 41 | 42 |
43 | 44 |
45 | 46 |
47 | 48 |
49 | 50 |
51 |
52 | 53 |
54 |
55 | 56 |
57 | -------------------------------------------------------------------------------- /app/components/app.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | angular 7 | .module('lrSpaApp', [ 8 | 'ngSanitize', 9 | 'hc.slider', 10 | 'hc.dsv', 11 | 'debounce', 12 | 'panels', 13 | 'ngAnimate', 14 | 'ui.router', 15 | 'localytics.directives', 16 | 'chieffancypants.loadingBar', 17 | 'LocalStorageModule', 18 | 'ui.bootstrap', 19 | 'hc.downloader', 20 | 'angular-growl', 21 | 'pasvaz.bindonce', 22 | 'ng-context-menu', 23 | 'ngGrid', 24 | 'hc.marked', 25 | 'ngClipboard' 26 | ]) 27 | 28 | .constant('site', { 29 | name: 'ligand-receptor-connectome', 30 | version: '0.1.0', 31 | apiVersion: 'lr-1', 32 | debug: false 33 | }) 34 | 35 | .run(function($rootScope, site) { 36 | $rootScope.site = site; 37 | }) 38 | 39 | .run(function($window, site) { 40 | if (site.debug) { 41 | $window.watchCount = function () { 42 | var i, data, scope, 43 | count = 0, 44 | all = document.all, 45 | len = all.length, 46 | test = {}; 47 | 48 | for (i=0; i < len; i++) { 49 | data = angular.element(all[i]).data(); 50 | if (data && data.hasOwnProperty('$scope') && data.$scope.$$watchers) { 51 | scope = data.$scope; 52 | if ( ! test[ scope.$id ] ) { 53 | test[ scope.$id ] = true; 54 | count += scope.$$watchers.length; 55 | } 56 | } 57 | } 58 | return count; 59 | }; 60 | } 61 | }) 62 | 63 | .config(function(localStorageServiceProvider, site){ 64 | localStorageServiceProvider.setPrefix(site.apiVersion); 65 | }) 66 | 67 | .config(function($logProvider, site) { 68 | $logProvider.debugEnabled(site.debug); 69 | }) 70 | 71 | .config(['growlProvider', function(growlProvider) { 72 | growlProvider.globalTimeToLive(5000); 73 | }]); 74 | 75 | 76 | 77 | })(); 78 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lrspa", 3 | "version": "0.0.7", 4 | "dependencies": { 5 | "angular": "1.2.23", 6 | "json3": "~3.3.2", 7 | "es5-shim": "~4.0.3", 8 | "jquery": "~1.11.0", 9 | "bootstrap": "~3.2.0", 10 | "d3": "~3.4.5", 11 | "angular-loading-bar": "~0.5.0", 12 | "angular-bootstrap": "~0.11.0", 13 | "angular-ui-router": "~0.2.10", 14 | "ng-debounce": "~0.1.1", 15 | "d3-plugins": "*", 16 | "angular-downloadsvg-directive": "~0.0.4", 17 | "angular-growl": "~0.4.0", 18 | "angular-animate": "~1.2.16", 19 | "angular-slider-widget": "Hypercubed/angular-slider-widget#~0.0.2", 20 | "angular-local-storage": "https://github.com/grevory/angular-local-storage.git#master", 21 | "_F": "Hypercubed/_F#~0.0.2", 22 | "lodash": "~2.4.1", 23 | "angular-dsv": "~0.0.4", 24 | "angular-sanitize": "~1.4.0", 25 | "angular-bindonce": "~0.3.1", 26 | "ng-context-menu": "~0.1.4", 27 | "ng-grid": "~2.0.14", 28 | "angular-marked": "~0.0.12", 29 | "font-awesome": "~4.2.0", 30 | "Blob.js": "eligrey/Blob.js", 31 | "js-base64": "~2.1.5", 32 | "angular-chosen-localytics": "~1.0.6", 33 | "ng-clip": "~0.2.2" 34 | }, 35 | "devDependencies": { 36 | "angular-mocks": "1.2.15", 37 | "angular-scenario": "1.2.15" 38 | }, 39 | "overrides": { 40 | "d3-plugins": { 41 | "main": [ 42 | "hive/hive.js" 43 | ] 44 | }, 45 | "marked": { 46 | "main": [ 47 | "lib/marked.js" 48 | ] 49 | }, 50 | "angular-growl": { 51 | "main": [ 52 | "./build/angular-growl.js", 53 | "./build/angular-growl.min.css" 54 | ] 55 | }, 56 | "ng-context-menu": { 57 | "main": [ 58 | "dist/ng-context-menu.js" 59 | ] 60 | }, 61 | "chosen": { 62 | "main": [ 63 | "chosen.jquery.js", 64 | "chosen.css" 65 | ] 66 | }, 67 | "Blob.js": { 68 | "main": [ 69 | "Blob.js" 70 | ] 71 | } 72 | }, 73 | "resolutions": { 74 | "angular": "1.2.23", 75 | "angular-sanitize": "~1.2.18" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/components/ui/panel-directives.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var baseUrl = 'components/ui/'; 5 | 6 | var app = angular.module('panels',[]); 7 | 8 | app 9 | .directive('panel', function($timeout) { 10 | return { 11 | scope: { 12 | heading: '@', 13 | label: '@', 14 | isOpen: '=?' 15 | }, 16 | restrict: 'EA', 17 | transclude: true, // Grab the contents to be used as the heading 18 | templateUrl: baseUrl+'panel.html', 19 | //controller: function() { 20 | // 21 | // this.setLabel = function(element) { 22 | // this.label = element; 23 | // }; 24 | 25 | //}, 26 | link: function(scope) { 27 | 28 | scope.ngIf = scope.isOpen; 29 | 30 | scope.toggleOpen = function() { 31 | 32 | if (scope.isOpen) { 33 | scope.isOpen = false; 34 | $timeout(function() { 35 | scope.ngIf = false; 36 | }); 37 | } else { 38 | scope.ngIf = true; 39 | $timeout(function() { 40 | scope.isOpen = true; 41 | }); 42 | } 43 | 44 | }; 45 | 46 | } 47 | }; 48 | }); 49 | 50 | /* app 51 | .directive('panelLabel', function() { 52 | return { 53 | restrict: 'EA', 54 | transclude: true, // Grab the contents to be used as the heading 55 | template: '', // In effect remove this element! 56 | replace: true, 57 | require: '^panel', 58 | link: function(scope, element, attr, ctrl, transclude) { 59 | ctrl.setLabel(transclude(scope, function() {})); 60 | } 61 | }; 62 | }); 63 | 64 | app 65 | .directive('panelTransclude', function() { 66 | return { 67 | require: '^panel', 68 | link: function(scope, element, attr, controller) { 69 | scope.$watch(function() { return controller[attr.panelTransclude]; }, function(heading) { 70 | if ( heading ) { 71 | element.html(''); 72 | element.append(heading); 73 | } 74 | }); 75 | } 76 | }; 77 | }); */ 78 | 79 | })(); 80 | -------------------------------------------------------------------------------- /app/components/charts/chart-legend-chart.js: -------------------------------------------------------------------------------- 1 | /* global _F */ 2 | /* global d3 */ 3 | 4 | (function(root) { 5 | 'use strict'; 6 | 7 | var chartLegend = function() { 8 | 9 | var _name = _F('name'); 10 | 11 | var colorScale; 12 | 13 | var dispatch = d3.dispatch('mouseover','mouseout'); 14 | 15 | function chart(selection) { 16 | selection.each(chart.draw); 17 | } 18 | 19 | chart.draw = function draw(data) { 20 | var container = d3.select(this); 21 | 22 | var labels = container.selectAll('.label').data(data).enter().append('g') 23 | .attr('class', 'label') 24 | .on('mouseover', function(d) { 25 | labels.style('opacity', function(_d) { return _d === d ? 1 : 0.2; }); 26 | dispatch.mouseover.apply(this,arguments); 27 | }) 28 | .on('mouseout', function() { 29 | labels.style('opacity', 1); 30 | dispatch.mouseout.apply(this,arguments); 31 | }) 32 | .attr('transform', function(d,i) { return 'translate(0,'+((i+1)*20)+')'; }); 33 | 34 | //var _rx = function rx(d) { return (d.group.match(/gene/)) ? 0 : 15; }; // todo: look at this... 35 | 36 | function _t(d) { 37 | if (d.group === 'gene.ligand') { return 'A'; } 38 | if (d.group === 'gene.receptor') { return 'V'; } 39 | return 'circle'; 40 | } 41 | 42 | var sym = root.models.hiveSymbol().type(_t).size(120); 43 | 44 | labels.append('path') 45 | .style({'stroke-width': '1px', 'stroke': 'rgb(51, 51, 51)'}) 46 | //.attr('width', 15) 47 | //.attr('height', 15) 48 | //.attr('rx',_rx) 49 | //.attr('ry',_rx) 50 | .attr('d', sym) 51 | .style('fill', colorScale) 52 | ; 53 | 54 | labels.append('text') 55 | .style({'stroke': 'none','fill': '#333','stroke-width': '1px','font-size': '10px'}) 56 | .attr('x', 20) 57 | .attr('dy', '0.4em') 58 | .text(_name); 59 | 60 | }; 61 | 62 | chart.colors = function(_) { 63 | if (arguments.length < 1) {return colorScale;} 64 | colorScale = _; 65 | return chart; 66 | }; 67 | 68 | //d3.rebind(chart, dispatch, 'on'); 69 | return d3.rebind(chart, dispatch, 'on'); 70 | }; 71 | 72 | root.models.legend = chartLegend; 73 | 74 | })(window.lrd3); 75 | -------------------------------------------------------------------------------- /app/components/ui/pageslide.css: -------------------------------------------------------------------------------- 1 | .pageslide { 2 | z-index: 1000; 3 | position: fixed; 4 | width: 350px; 5 | height: 100%; 6 | top: 0px; 7 | bottom: 0px; 8 | -moz-transition-property: all; /* Firefox 4 */ 9 | -webkit-transition-property: all; /* Safari and Chrome */ 10 | -o-transition-property: all; /* Opera */ 11 | transition-property: all; 12 | -webkit-transition-duration: 0.5s; /* Safari */ 13 | transition-duration: 0.5s; 14 | background: #eee; 15 | padding: 0 3px; 16 | box-shadow: 0px 0px 5px #888888; 17 | } 18 | 19 | .pageslide { 20 | display: block !important; 21 | } 22 | 23 | .pageslide-toggle { 24 | z-index: 1010; 25 | } 26 | 27 | .pageslide-toggle.right { 28 | position: absolute; 29 | left: -40px; 30 | top: 5px; 31 | width: 34px; 32 | } 33 | 34 | .pageslide-toggle button { 35 | opacity: 0.5; 36 | } 37 | 38 | .pageslide-toggle button:hover { 39 | opacity: 1; 40 | } 41 | 42 | .pageslide-toggle.bottom { 43 | position: absolute; 44 | top: 5px; 45 | right: 5px; 46 | } 47 | 48 | .pageslide-right { 49 | right: 10px; 50 | } 51 | 52 | .pageslide-left { 53 | left: 0px; 54 | } 55 | 56 | .pageslide-bottom { 57 | left: 0px; 58 | right: 0px; 59 | top: auto; 60 | bottom: 0px; 61 | height: 400px; 62 | width: auto; 63 | } 64 | 65 | .pageslide-bottom .tab-pane { 66 | overflow-y: auto; 67 | overflow-x: hidden; 68 | height: 350px; 69 | } 70 | 71 | .pageslide-bottom .gridStyle { 72 | border: 1px solid rgb(212,212,212); 73 | width: 100%; 74 | height: 340px; 75 | } 76 | 77 | .pageslide.ng-hide > div, .pageslide.ng-hide-remove > div { 78 | /* display: none; */ 79 | } 80 | 81 | .pageslide-bottom.ng-hide > div, .pageslide-bottom.ng-hide-remove > div { 82 | display: block; 83 | } 84 | 85 | .pageslide-right.ng-hide { 86 | -webkit-transform: translateX(335px); /* Chrome, Safari, Opera */ 87 | transform: translateX(335px); 88 | } 89 | 90 | .pageslide-left.ng-hide { 91 | /* width: 0px; */ 92 | -webkit-transform: translateX(-345px); /* Chrome, Safari, Opera */ 93 | transform: translateX(-345px); 94 | } 95 | 96 | .pageslide-bottom.ng-hide { 97 | /* width: 0px; */ 98 | -webkit-transform: translateY(360px); /* Chrome, Safari, Opera */ 99 | transform: translateY(360px); 100 | } 101 | 102 | .ngViewport { 103 | overflow-y: auto; 104 | min-height: 20px; 105 | overflow-x: hidden; 106 | } 107 | -------------------------------------------------------------------------------- /app/components/ui/right-panel.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
5 | 6 |
7 | 8 | 18 | 19 | 22 | 23 | 27 | 28 | 32 | 33 | 37 | 38 | 42 | 43 |
44 | 45 |
47 |
48 |
    49 |
  • Nodes: {{graphData._nodes.length}} / {{graphData.nodes.length}}
  • 50 |
  • Edges: {{graphData._edges.length}} / {{graphData.edges.length}}
  • 51 |
52 |
53 | 54 |
55 | 56 |
57 |
58 |
59 |
60 | 61 |
62 |
63 | 64 | 69 | 70 | 75 | -------------------------------------------------------------------------------- /app/components/charts/chart.css: -------------------------------------------------------------------------------- 1 | /* #vis { text-align: center; width: 700px;} 2 | #vis svg { width: 100%; height: 500px; } */ 3 | 4 | #vis svg { 5 | position: absolute; 6 | top: 0; 7 | bottom: 0; 8 | right: 0; 9 | width: 100%; 10 | height: 100%; 11 | 12 | text-rendering: optimizeLegibility; 13 | shape-rendering: geometricPrecision; 14 | 15 | font-family: Helvetica, Arial, sans-serif; 16 | } 17 | 18 | .axis { 19 | stroke: #000; 20 | stroke-width: 1.5px; 21 | } 22 | 23 | .node { 24 | fill: #ccc; 25 | fill-opacity: 1; 26 | stroke: #333; 27 | stroke-width: 1px; 28 | cursor: pointer; 29 | } 30 | 31 | .node text { 32 | stroke: none; 33 | fill: #333; 34 | stroke-width: 1px; 35 | font-size: 10px; 36 | cursor: pointer; 37 | display: none; 38 | /* shape-rendering: crispEdges; 39 | writing-mode: tb; 40 | glyph-orientation-vertical: 0; 41 | kerning: -3; */ 42 | } 43 | 44 | svg .label { 45 | cursor: pointer; 46 | } 47 | 48 | svg.labels .node text { 49 | display: inherit; 50 | } 51 | 52 | .node:hover text, .node.hover text, .node.fixed text { 53 | font-size: 18px !important; 54 | } 55 | 56 | .node.fixed circle, .node.fixed rect { 57 | stroke: red !important; 58 | stroke-width: 3px !important; 59 | } 60 | 61 | .link { 62 | fill: none; 63 | stroke: #666; 64 | stroke-width: 1.5px; 65 | opacity: 0.6; 66 | cursor: pointer; 67 | } 68 | 69 | svg.fixed .link { 70 | stroke: #666; 71 | opacity: 0.3; 72 | } 73 | 74 | svg.hover .link { 75 | stroke: #666 !important; 76 | opacity: 0.1 !important; 77 | } 78 | 79 | svg.hover .link.hover, .link:hover { 80 | stroke: blue !important; 81 | opacity: 1 !important; 82 | } 83 | 84 | svg.hover .node { 85 | opacity: 0.2 !important; 86 | } 87 | 88 | svg.hover .node.hover, svg.hover .node:hover { 89 | opacity: 1 !important; 90 | } 91 | 92 | .link.fixed { 93 | stroke: red !important; 94 | opacity: 1 !important; 95 | } 96 | 97 | .target-arrow { 98 | stroke-width: 1px; 99 | stroke: #666; 100 | fill: none; 101 | opacity: 1; 102 | } 103 | 104 | #legend { 105 | margin: 0 30px; 106 | display: inline-block; 107 | } 108 | 109 | #legend .rotate { 110 | height:30px; 111 | line-height: 30px; 112 | } 113 | 114 | #legend .node { 115 | display: inline-block; 116 | border: 0; 117 | width: 16px; 118 | height: 16px; 119 | margin-top: 0; 120 | border-radius: 8px; 121 | } 122 | 123 | #legend .node.square { 124 | border-radius: 0; 125 | } 126 | 127 | #legend .node.receptor { 128 | background-color: #3349ff; 129 | } 130 | 131 | #legend .node.ligand { 132 | background-color: #ed1940; 133 | } 134 | 135 | #legend .node.both { 136 | background-color: #a650e2; 137 | } 138 | 139 | .d3-tip { 140 | line-height: 1; 141 | font-weight: bold; 142 | padding: 12px; 143 | background: rgba(0, 0, 0, 0.8); 144 | color: #fff; 145 | border-radius: 2px; 146 | pointer-events:none !important; 147 | z-index: 100; 148 | position: absolute; 149 | top: 40px !important; 150 | right: 20px !important; 151 | width: 400px !important; 152 | left: auto !important; 153 | } 154 | -------------------------------------------------------------------------------- /app/components/ui/main-directives.js: -------------------------------------------------------------------------------- 1 | 2 | (function() { 3 | 'use strict'; 4 | 5 | angular.module('lrSpaApp') 6 | 7 | .filter('percentage', ['$filter', function($filter) { 8 | return function(input, decimals) { 9 | return $filter('number')(input*100, decimals)+'%'; 10 | }; 11 | }]) 12 | 13 | .filter('min', function() { 14 | return function(input) { 15 | var out; 16 | if (input) { 17 | for (var i in input) { 18 | if (input[i] < out || out === undefined || out === null) { 19 | out = input[i]; 20 | } 21 | } 22 | } 23 | return out; 24 | }; 25 | }) 26 | 27 | .filter('max', function() { 28 | return function(input) { 29 | var out; 30 | if (input) { 31 | for (var i in input) { 32 | if (input[i] > out || out === undefined || out === null) { 33 | out = input[i]; 34 | } 35 | } 36 | } 37 | return out; 38 | }; 39 | }) 40 | 41 | .directive('graphItem', function() { 42 | return { 43 | scope: { 44 | item: '=graphItem', 45 | data: '=', 46 | graphData: '=' 47 | }, 48 | templateUrl: 'components/ui/item.html' 49 | }; 50 | }) 51 | 52 | .directive('neighborsList', function() { 53 | return { 54 | scope: { 55 | list: '=neighborsList', 56 | array: '=', 57 | title: '&', 58 | key: '&' 59 | }, 60 | templateUrl: 'components/ui/neighbors-list-template.html', 61 | link: function (scope, element, attrs) { 62 | scope.limit = 3; 63 | scope.key = attrs.key; 64 | 65 | scope.hover = function(item, __) { 66 | if (item.ticked) { 67 | item.hover = __; 68 | } 69 | }; 70 | 71 | scope.click = function(item) { 72 | item.ticked = !item.ticked; 73 | }; 74 | 75 | scope.expand = function() { 76 | scope.limit = (scope.limit + 10 < scope.list.length) ? scope.limit + 10 : scope.list.length; 77 | }; 78 | 79 | } 80 | }; 81 | }) 82 | 83 | .directive('expressionList', function() { 84 | return { 85 | scope: { 86 | list: '=expressionList', 87 | array: '=', 88 | key: '&' 89 | }, 90 | templateUrl: 'components/ui/gene-list-template.html', 91 | link: function (scope, element, attrs) { 92 | scope.limit = 3; 93 | 94 | attrs.key = attrs.key || 'gene'; // todo: change 95 | scope.key = attrs.key; 96 | 97 | scope.get = function(_) { 98 | var __ = _[attrs.key]; 99 | 100 | if (typeof __ === 'number' && attrs.array) { // this is an id 101 | return scope.array[__]; 102 | } else { 103 | return __; 104 | } 105 | }; 106 | 107 | scope.hover = function(item, __) { 108 | if (item.ticked) { 109 | item.hover = __; 110 | } 111 | }; 112 | 113 | scope.click = function(item) { 114 | item.ticked = !item.ticked; 115 | }; 116 | 117 | scope.expand = function() { 118 | scope.limit = (scope.limit + 10 < scope.list.length) ? scope.limit + 10 : scope.list.length; 119 | }; 120 | 121 | } 122 | }; 123 | }); 124 | 125 | })(); 126 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: 'app', 8 | 9 | // testing framework to use (jasmine/mocha/qunit/...) 10 | frameworks: ['jasmine'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'bower_components/jquery/dist/jquery.js', 15 | 'bower_components/angular/angular.js', 16 | 'bower_components/angular-mocks/angular-mocks.js', 17 | 'bower_components/_F/_F.js', 18 | 'bower_components/d3/d3.js', 19 | 'components/charts/core.js', 20 | 'bower_components/angular-sanitize/angular-sanitize.js', 21 | 'bower_components/angular-loading-bar/src/loading-bar.js', 22 | 'bower_components/angular-bootstrap/ui-bootstrap-tpls.js', 23 | 'bower_components/angular-ui-router/release/angular-ui-router.js', 24 | 'bower_components/ng-debounce/angular-debounce.js', 25 | //'bower_components/d3-plugins/hive/hive.js', 26 | //'bower_components/FileSaver/FileSaver.js', 27 | 'bower_components/angular-downloadsvg-directive/angular-downloadsvg-directive.js', 28 | 'bower_components/angular-growl/build/angular-growl.js', 29 | 'bower_components/angular-animate/angular-animate.js', 30 | 'bower_components/angular-slider-widget/angular-slider-widget.js', 31 | 'bower_components/angular-local-storage/angular-local-storage.js', 32 | 'bower_components/_F/_F.js', 33 | //'bower_components/lodash/dist/lodash.compat.js', 34 | 'bower_components/angular-dsv/angular-dsv.js', 35 | 'bower_components/angular-sanitize/angular-sanitize.js', 36 | 'bower_components/angular-bindonce/bindonce.js', 37 | 'bower_components/ng-context-menu/dist/ng-context-menu.js', 38 | 'bower_components/ng-grid/build/ng-grid.js', 39 | //'bower_components/marked/lib/marked.js', 40 | 'bower_components/angular-marked/angular-marked.js', 41 | //'bower_components/Blob.js/Blob.js', 42 | //'bower_components/chosen/chosen.jquery.js', 43 | 'bower_components/angular-chosen-localytics/chosen.js', 44 | 'bower_components/zeroclipboard/dist/ZeroClipboard.js', 45 | 'bower_components/ng-clip/dest/ng-clip.min.js', 46 | 'bower_components/ng-grid/plugins/ng-grid-flexible-height.js', 47 | 'components/*.js', 48 | 'components/**/*.js', 49 | //'test/mock/**/*.js', 50 | '../test/spec/**/*.js', 51 | {pattern: 'data/*.txt', watched: true, served: true, included: false} 52 | ], 53 | 54 | // list of files / patterns to exclude 55 | exclude: [], 56 | 57 | reporters: ['mocha'], 58 | 59 | // web server port 60 | port: 8080, 61 | 62 | // level of logging 63 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 64 | logLevel: config.LOG_INFO, 65 | 66 | // enable / disable watching file and executing tests whenever any file changes 67 | autoWatch: false, 68 | 69 | // Start these browsers, currently available: 70 | // - Chrome 71 | // - ChromeCanary 72 | // - Firefox 73 | // - Opera 74 | // - Safari (only Mac) 75 | // - PhantomJS 76 | // - IE (only Windows) 77 | browsers: ['PhantomJS'], 78 | 79 | // Continuous Integration mode 80 | // if true, it capture browsers, run tests and exit 81 | singleRun: false 82 | }); 83 | }; 84 | -------------------------------------------------------------------------------- /app/data/ontology.txt: -------------------------------------------------------------------------------- 1 | Cell Ontology 2 | Adipocyte Breast mesenchymal 3 | Adipocyte Omental mesenchymal 4 | Adipocyte Perirenal mesenchymal 5 | Adipocyte Subcutaneous mesenchymal 6 | Alveolar Epithelial epithelial 7 | Amniotic Epithelial epithelial 8 | Amniotic Membrane epithelial 9 | Annulus Pulposus other 10 | Astrocyte Cerebellum nervous system 11 | Astrocyte Cerebral Cortex nervous system 12 | Basophils hematopoietic 13 | Bronchial Epithelial epithelial 14 | Cardiac Myocyte mesenchymal 15 | CD133+ Haematopoietic stem cell hematopoietic 16 | CD14-CD16+ Monocytes hematopoietic 17 | CD14+ Monocyte derived Endothelial progenitor hematopoietic 18 | CD14+ Monocytes hematopoietic 19 | CD14+CD16- Monocytes hematopoietic 20 | CD14+CD16+ Monocytes hematopoietic 21 | CD19+ B cells hematopoietic 22 | CD34+ Haematopoietic stem cell hematopoietic 23 | CD34+ progenitor hematopoietic 24 | CD4+ T cells hematopoietic 25 | CD4+CD25-CD45RA- memory conventional T cells hematopoietic 26 | CD4+CD25-CD45RA+ naive conventional T cells hematopoietic 27 | CD4+CD25+CD45RA- memory regulatory T cells hematopoietic 28 | CD4+CD25+CD45RA+ naive regulatory T cells hematopoietic 29 | CD8+ T cells hematopoietic 30 | Chondroblast mesenchymal 31 | Chondrocyte mesenchymal 32 | Chorionic Membrane epithelial 33 | Ciliary Epithelial epithelial 34 | Corneal Epithelial epithelial 35 | Dendritic Monocyte Immature derived hematopoietic 36 | Dendritic Plasmacytoid hematopoietic 37 | ES cells other 38 | Endothelial Aortic endothelial 39 | Endothelial Artery endothelial 40 | Endothelial Lymphatic endothelial 41 | Endothelial Microvascular endothelial 42 | Endothelial Thoracic endothelial 43 | Endothelial Umbilical Vein endothelial 44 | Endothelial Vein endothelial 45 | Eosinophils hematopoietic 46 | Esophageal Epithelial epithelial 47 | Fibroblast Aortic Adventitial mesenchymal 48 | Fibroblast Cardiac mesenchymal 49 | Fibroblast Choroid Plexus mesenchymal 50 | Fibroblast Conjunctival mesenchymal 51 | Fibroblast Dermal mesenchymal 52 | Fibroblast Gingival mesenchymal 53 | Fibroblast Lymphatic mesenchymal 54 | Fibroblast Periodontal Ligament mesenchymal 55 | Fibroblast Pulmonary Artery mesenchymal 56 | Fibroblast Skin Normal mesenchymal 57 | Gingival Epithelial epithelial 58 | Granulocyte Macrophage progenitor hematopoietic 59 | Hair Follicle Dermal Papilla epithelial 60 | Hair Follicle Outer Root Sheath epithelial 61 | Hepatic Sinusoidal Endothelial endothelial 62 | Hepatic Stellate mesenchymal 63 | Hepatocyte epithelial 64 | Immature Langerhans hematopoietic 65 | Intestinal Epithelial (polarized) epithelial 66 | Iris Pigment Epithelial epithelial 67 | Keratinocyte Epidermal epithelial 68 | Keratinocyte Oral epithelial 69 | Keratocytes epithelial 70 | Lens Epithelial epithelial 71 | Macrophage Monocyte derived hematopoietic 72 | Mallassez derived epithelial 73 | Mammary Epithelial epithelial 74 | Mast cells hematopoietic 75 | Mast cells stimulated hematopoietic 76 | Mature Adipocyte mesenchymal 77 | Melanocyte epithelial 78 | Meningeal other 79 | Mesenchymal precursor Adipose mesenchymal 80 | Mesenchymal precursor Bone Marrow mesenchymal 81 | Mesenchymal precursor Cardiac mesenchymal 82 | Mesenchymal stem cell Adipose mesenchymal 83 | Mesenchymal stem cell Amniotic Membrane mesenchymal 84 | Mesenchymal stem cell Bone Marrow mesenchymal 85 | Mesenchymal stem cell Hepatic mesenchymal 86 | Mesenchymal stem cell Umbilical mesenchymal 87 | Mesenchymal stem cell Wharton's Jelly mesenchymal 88 | Mesothelial mesenchymal 89 | Migratory Langerhans hematopoietic 90 | Multipot. Cord Blood Unrestrict. Somatic stem cell hematopoietic 91 | Myoblast mesenchymal 92 | Myotube mesenchymal 93 | Nasal Epithelial epithelial 94 | NK cells hematopoietic 95 | Neural stem cell nervous system 96 | Neurons nervous system 97 | Neutrophils hematopoietic 98 | Nucleus Pulposus mesenchymal 99 | Olfactory Epithelial epithelial 100 | Osteoblast mesenchymal 101 | Osteocyte mesenchymal 102 | Pancreatic Stromal mesenchymal 103 | Pericytes mesenchymal 104 | Placental Epithelial epithelial 105 | Preadipocyte Breast mesenchymal 106 | Preadipocyte Omental mesenchymal 107 | Preadipocyte Perirenal mesenchymal 108 | Preadipocyte Subcutaneous mesenchymal 109 | Preadipocyte Visceral mesenchymal 110 | Prostate Epithelial epithelial 111 | Prostate Stromal mesenchymal 112 | Renal Cortical Epithelial epithelial 113 | Renal Epithelial epithelial 114 | Renal Glomerular Endothelial endothelial 115 | Renal Mesangial mesenchymal 116 | Renal Proximal Tubular Epithelial epithelial 117 | Reticulocyte hematopoietic 118 | Retinal Pigment Epithelial epithelial 119 | Salivary Acinar epithelial 120 | Sebocyte epithelial 121 | Sertoli other 122 | Skeletal Muscle mesenchymal 123 | Skeletal Muscle Satellite mesenchymal 124 | Small Airway Epithelial epithelial 125 | Smooth Muscle Aortic mesenchymal 126 | Smooth Muscle Brachiocephalic mesenchymal 127 | Smooth Muscle Brain Vascular mesenchymal 128 | Smooth Muscle Bronchial mesenchymal 129 | Smooth Muscle Carotid mesenchymal 130 | Smooth Muscle Colonic mesenchymal 131 | Smooth Muscle Coronary Artery mesenchymal 132 | Smooth Muscle Esophageal mesenchymal 133 | Smooth Muscle Internal Thoracic Artery mesenchymal 134 | Smooth Muscle Prostate mesenchymal 135 | Smooth Muscle Pulmonary Artery mesenchymal 136 | Smooth Muscle Subclavian Artery mesenchymal 137 | Smooth Muscle Tracheal mesenchymal 138 | Smooth Muscle Umbilical Artery mesenchymal 139 | Smooth Muscle Umbilical Vein mesenchymal 140 | Smooth Muscle Uterine mesenchymal 141 | Synoviocyte mesenchymal 142 | Tenocyte mesenchymal 143 | Trabecular Meshwork other 144 | Tracheal Epithelial epithelial 145 | Urothelial epithelial 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # connectome 2 | 3 | Web tool for exploring cell-cell ligand-receptor mediated communication networks 4 | 5 | ## Introduction 6 | 7 | In Ramilowski et al. ‘A draft network of ligand-receptor mediated multicellular signaling in human’ 2015 [doi:10.1038/ncomms8866](http://dx.doi.org/10.1038/ncomms8866) we present the first large-scale map of cell-to-cell communication between 144 human primary cell types using 2,422 putative and literature supported ligand-receptor pairs. With up to hundreds of potential interactions between any two of these 144 primary cell types, there are millions of possible cell-cell communication paths across the entire network. Static visualization of such complex networks not only can be obscure and impractical but also difficult. With that, and to benefit the research community, we provide an online resource that visualizes, on demand, our cell-cell communication network for any given subset of the ligand-receptor pairs and profiled primary cells. An online version of the resource is located at: [Ramilowski_et_al_2015](http://fantom.gsc.riken.jp/5/suppl/Ramilowski_et_al_2015/vis/) and mirrored at [forrest-lab.github.io/connectome](http://forrest-lab.github.io/connectome). 8 | 9 | We developed the online connectome visualization application using various open source and custom tools. The vector graphic visualization is generated using the [D3.js visualization library][d3]. The application interface was developed using the [AngularJS web application framework][angular] and the [twitter bootstrap front-end framework][twbs]. 10 | 11 | The visualization interface takes the the expression files generated in this study along with other metadata in tabular format [Ramilowski_et_al_2015](http://fantom.gsc.riken.jp/5/suppl/Ramilowski_et_al_2015/) to generate the network/hive visualization as shown in figure 5 in the paper. 12 | 13 | # For Developers 14 | 15 | ## Background 16 | 17 | To install a copy of this application you will need [node and npm](http://nodejs.org/), Grunt, and Bower. If you are not familiar it would be worthwhile to read up on [node and npm](http://www.joyent.com/blog/installing-node-and-npm/), [Grunt](https://github.com/gruntjs/grunt/wiki/Getting-started) and [bower](http://bower.io/). 18 | 19 | ## Download and Install 20 | 21 | ``` 22 | git clone https://github.com/Hypercubed/connectome.git 23 | cd connectome 24 | npm install 25 | bower install 26 | ``` 27 | 28 | ## Summary of Directory Layout 29 | 30 | app/ --> all of the files to be used in development 31 | bower_components/ --> AngularJS and 3rd party JavaScript libraries installed using bower 32 | components/ --> Application components 33 | data/ --> data files 34 | test/ --> test source files and libraries 35 | package.json --> npm's config file 36 | bower.json --> bower's config file 37 | Gruntfile.js --> Grunt config file 38 | README.md --> This file 39 | 40 | ## Adding data 41 | 42 | This git repository include the data files that acompany the above referenced paper. You add your own into the app/data/ folder. All data files should be Tab-Seperated-Value (TSV) files. 43 | 44 | ## Grunt 45 | 46 | Grunt is a JavaScript based task runner. In this project Grunt is used for many tasks including testing, minification, and even deployment. If you are not familiar with Grunt please read the [Getting started guide](https://github.com/gruntjs/grunt/wiki/Getting-started). 47 | 48 | Summary of Grunt tasks: 49 | 50 | clean Clean files and folders. 51 | test Run all tests 52 | build Prepare project for deployment. 53 | serve Run a test server 54 | deploy Build and deploy to github 55 | 56 | ## Running the app during development 57 | 58 | Running `grunt serve` will run a test server on the local host and open your default web browser to http://localhost:9000/. 59 | 60 | # Contact 61 | 62 | For more information please contact J. Harshbarger 63 | 64 | ## Acknowledgments 65 | 66 | This work was supported by a research grant from the Japanese Ministry of Education, Culture, Sports, Science and Technology (MEXT) to the RIKEN Center for Life Science Technologies. 67 | 68 | ## Reference 69 | 70 | > **A draft network of ligand–receptor-mediated multicellular signalling in human** 71 | 72 | > Jordan A. Ramilowski, Tatyana Goldberg, Jayson Harshbarger, Edda Kloppman, Marina Lizio, Venkata P. Satagopam, Masayoshi Itoh, Hideya Kawaji, Piero Carninci, Burkhard Rost & Alistair R. R. Forrest 73 | 74 | > Nature Communications 6, Article number: 7866 [doi:10.1038/ncomms8866](http://doi.org/10.1038/ncomms8866) 75 | 76 | ## License 77 | 78 | [MIT License](http://en.wikipedia.org/wiki/MIT_License) 79 | 80 | Copyright (c) 2015 RIKEN, Japan. 81 | 82 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 83 | 84 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 85 | 86 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 87 | 88 | 89 | [d3]: http://d3js.org/ "Data-Driven Documents" 90 | [angular]: http://angularjs.org/ "AngularJS Framework" 91 | [twbs]: http://getbootstrap.com/2.3.2/ "Twitter bootstrap" 92 | -------------------------------------------------------------------------------- /app/components/ui/panel-controller.js: -------------------------------------------------------------------------------- 1 | 2 | (function() { 3 | 'use strict'; 4 | 5 | angular.module('lrSpaApp') 6 | 7 | .controller('PanelCtrl', function ($scope, localStorageService) { 8 | 9 | this.state = { 10 | info: false, 11 | data: false 12 | }; 13 | 14 | // Panel state 15 | localStorageService.bind($scope, 'panelState', this.state); 16 | 17 | $scope.gridOptions = {}; 18 | 19 | $scope.itemClicked = function(row) { 20 | //console.log(row.selectionProvider.selectedItems); 21 | 22 | if (row.entity.locked) { 23 | row.entity.ticked = false; 24 | } 25 | 26 | //if(row.entity.receptor && row.entity.ligand) { 27 | // row.entity.receptor.ticked = row.entity.ticked; 28 | // row.entity.ligand.ticked = row.entity.ticked; 29 | //} 30 | 31 | if (row.selected === true) { 32 | row.selectionProvider.selectedItems.forEach(function(d) { 33 | d.ticked = row.entity.ticked; 34 | d.locked = row.entity.locked; 35 | 36 | //if(d.receptor && d.ligand) { 37 | // d.receptor.ticked = row.entity.ticked; 38 | // d.ligand.ticked = row.entity.ticked; 39 | //} 40 | 41 | }); 42 | } 43 | }; 44 | 45 | var defaults = { 46 | showFooter: true, 47 | enableSorting: true, 48 | multiSelect: true, 49 | showFilter: true, 50 | showColumnMenu: false, 51 | showGroupPanel: false, 52 | enableCellSelection: false, 53 | selectWithCheckboxOnly: false, 54 | showSelectionCheckbox: true, 55 | enableColumnResize: true, 56 | sortInfo: { fields: ['2*locked + !ticked'], directions: ['asc'] }, 57 | //groups: ['locked'], 58 | //groupsCollapsedByDefault: true, 59 | //rowTemplate: 'rowTemplate', 60 | //menuTemplate: 'menuTemplate', 61 | //checkboxCellTemplate: '
', 62 | checkboxHeaderTemplate: 'checkboxHeaderTemplate.html', 63 | beforeSelectionChange: function(row, e) { // Without shift or ctrl deselect previous 64 | if (!angular.isArray(row) && !e.ctrlKey && !e.shiftKey) { 65 | row.selectionProvider.toggleSelectAll(false,true); 66 | } 67 | return true; 68 | }, 69 | columnDefs: [ 70 | { 71 | //field:'ticked', 72 | field: '2*locked + !ticked', 73 | displayName:'Visible', 74 | width: 60, 75 | cellTemplate: 'cellTemplate', 76 | headerCellTemplate: 'visibleHeaderCellTemplate' 77 | } 78 | ] 79 | }; 80 | 81 | $scope.gridOptions = {}; 82 | 83 | $scope.gridOptions.cells = angular.extend({}, defaults, { 84 | data: 'data.cells', 85 | columnDefs: [ 86 | defaults.columnDefs[0], 87 | {field:'name', displayName:'Cell Type'}, 88 | {field:'meta.Ontology', displayName:'Ontology'} 89 | ] 90 | }); 91 | 92 | $scope.gridOptions.genes = angular.extend({}, defaults, { 93 | data: 'data.genes', 94 | columnDefs: [ 95 | defaults.columnDefs[0], 96 | {field:'name', displayName:'Gene Symbol'}, 97 | {field:'description', width: '25%', displayName:'Gene Name'}, 98 | {field:'class', displayName:'Class'}, 99 | //{field:'age', displayName:'Age',cellFilter:'number'}, 100 | //{field:'consensus', displayName:'Subcellular Localization'}, 101 | {field:'hgncid', displayName:'HGNC ID',cellTemplate:'cellHGNCTemplate'}, 102 | {field:'uniprotid', displayName:'UniProt ID', cellTemplate:'cellUniProtTemplate'}, 103 | {field:'taxon', displayName:'Taxon'} 104 | ] 105 | }); 106 | 107 | $scope.gridOptions.pairs = angular.extend({}, defaults, { 108 | data: 'data.pairs', 109 | columnDefs: [ 110 | defaults.columnDefs[0], 111 | //{field:'ticked', displayName:'Visible', width: 60, cellTemplate: 'cellPairTemplate'}, 112 | {field:'name', displayName:'Pair Name'}, 113 | {field:'Ligand', displayName:'Ligand',cellTemplate: 'cellLigandTemplate'}, 114 | {field:'Receptor', displayName:'Receptor',cellTemplate: 'cellReceptorTemplate'}, 115 | {field:'Source', cellTemplate: 'cellSourceTemplate' /*, displayName:'Source' , */ }, 116 | {field:'Evidence', cellTemplate: 'cellPubMedTemplate' /*, displayName:'Evidence' */} 117 | ] 118 | }); 119 | 120 | //$scope.gridOptions.cells = angular.extend({}, defaultGridOptions,{ 121 | // data: 'data.cells', 122 | // columnDefs: defaultGridOptions.columnDefs.concat([ 123 | // {field:'meta.Ontology', displayName:'Ontology'} 124 | // ]) 125 | //}); 126 | 127 | /* $scope.gridOptions.genes = angular.extend({}, defaultGridOptions, { 128 | data: 'data.genes', 129 | columnDefs: defaultGridOptions.columnDefs.concat([ 130 | {field:'class', displayName:'Type'}, 131 | {field:'age', displayName:'Age'}, 132 | {field:'taxon', displayName:'Taxon',cellFilter:'number'}, 133 | {field:'consensus', displayName:'Consensus'}, 134 | {field:'description', displayName:'Description'}, 135 | {field:'hgncid', displayName:'HGNC ID'}, 136 | {field:'uniprotid', displayName:'UniProt ID'} 137 | ]) 138 | }); */ 139 | 140 | /* $scope.gridOptions.pairs = angular.extend({}, defaultGridOptions, { 141 | data: 'data.pairs', 142 | columnDefs: defaultGridOptions.columnDefs.concat([ 143 | {field:'Ligand', displayName:'Ligand',cellTemplate: 'cellLigandTemplate'}, 144 | {field:'Receptor', displayName:'Receptor',cellTemplate: 'cellReceptorTemplate'}, 145 | ]) 146 | }); */ 147 | 148 | }); 149 | 150 | })(); 151 | -------------------------------------------------------------------------------- /app/components/ui/item.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
{{item.name}} {{item.class}}
4 | 5 |
    6 | 12 | 18 | 19 |
  • Taxon:
  • 20 |
  • UniProtID:
  • 21 |
22 | 23 |
27 | 28 |
39 |
40 | 41 |
42 | 43 | 80 | 81 |
82 | 83 |
84 |
85 |
    91 | 111 |
  • 112 |
113 | 114 |
126 |
127 | 128 |
129 | 130 |
131 |
132 | 136 | 137 |
149 |
150 |
151 | 152 |
153 |
154 |
    155 |
  • Expression value: {{item.value | number:2}} TPM
  • 156 | 157 |
  • Specificity:
  • 158 |
159 |
160 | 161 |
162 |
163 |
164 | 165 |
166 | -------------------------------------------------------------------------------- /test/spec/services/ligandReceptorData.js: -------------------------------------------------------------------------------- 1 | /* global _F */ 2 | 3 | 'use strict'; 4 | 5 | describe('Service: ligandReceptorData', function () { 6 | 7 | var _name = _F('name'); 8 | var _median = _F('median'); 9 | 10 | // load the service's module 11 | beforeEach(module('lrSpaApp')); 12 | 13 | // instantiate service 14 | var $httpBackend, ligandReceptorData; 15 | 16 | beforeEach(function() { 17 | 18 | inject(function (_$httpBackend_, _ligandReceptorData_) { 19 | 20 | $httpBackend = _$httpBackend_; 21 | ligandReceptorData = _ligandReceptorData_; 22 | 23 | var pairs = ['Ligand','Receptor','Source','Databases'].join('\t')+'\n'; 24 | pairs += ['l1','r1','source1','none'].join('\t')+'\n'; 25 | pairs += ['l2','r2','source2','none'].join('\t'); 26 | 27 | var expr = ['gene','c1','c2','c3'].join('\t')+'\n'; 28 | expr += ['l1','0','100','11'].join('\t')+'\n'; 29 | expr += ['r1','11','0','0'].join('\t')+'\n'; 30 | expr += ['l2','9','0','0'].join('\t')+'\n'; 31 | expr += ['r2','5','11','0'].join('\t'); 32 | 33 | var ont = ['Cell','Ontology'].join('\t')+'\n'; 34 | ont += ['c1','mesenchymal'].join('\t')+'\n'; 35 | ont += ['c2','mesenchymal'].join('\t')+'\n'; 36 | ont += ['c3','mesenchymal'].join('\t'); 37 | 38 | var genes = ['ApprovedSymbol','ApprovedName','Class','UniProtID','HGNCID','Taxon'].join('\t')+'\n'; 39 | genes += ['l1','l1-desc','ligand','id','id','tax'].join('\t')+'\n'; 40 | genes += ['r1','r1-desc','receptor','id','id','tax'].join('\t')+'\n'; 41 | genes += ['l2','l2-desc','ligand','id','id','tax'].join('\t')+'\n'; 42 | genes += ['r2','r2-desc','receptor','id','id','tax'].join('\t')+'\n'; 43 | 44 | $httpBackend.expectGET('data/LR.pairs.txt').respond(pairs); 45 | $httpBackend.expectGET('data/LR.expr.txt').respond(expr); 46 | $httpBackend.expectGET('data/ontology.txt').respond(ont); 47 | $httpBackend.expectGET('data/LR.genes.txt').respond(genes); 48 | 49 | }); 50 | }); 51 | 52 | it('should load files', function () { 53 | 54 | ligandReceptorData.load().then(function(data) { 55 | expect(data.expr.length).toEqual(5); 56 | //expect(data.cells.length).toEqual(3); 57 | //expect(data.pairs.length).toEqual(2); 58 | //expect(data.genes.length).toEqual(4); 59 | 60 | 61 | expect(data.cells.map(_name)).toEqual(['c1','c2','c3']); 62 | expect(data.genes.map(_name)).toEqual(['l1','r1','l2','r2']); 63 | expect(data.pairs.map(_name)).toEqual(['l1-r1','l2-r2']); 64 | expect(data.genes.map(_median)).toEqual([11, 0, 0, 5]); 65 | }); 66 | 67 | $httpBackend.flush(); 68 | }); 69 | 70 | describe('getCells', function() { 71 | it('should return correct results', function () { 72 | 73 | ligandReceptorData.load().then(function() { 74 | expect(ligandReceptorData.getCells({}).length).toEqual(3); 75 | expect(ligandReceptorData.getCells({ name: 'c1' }).length).toEqual(1); 76 | expect(ligandReceptorData.getCells({ name: 'c4' }).length).toEqual(0); 77 | }); 78 | 79 | $httpBackend.flush(); 80 | }); 81 | }); 82 | 83 | describe('getGenes', function() { 84 | it('should return correct results', function () { 85 | 86 | ligandReceptorData.load().then(function() { 87 | expect(ligandReceptorData.getGenes({}).length).toEqual(4); 88 | expect(ligandReceptorData.getGenes({ name: 'l1' }).length).toEqual(1); 89 | expect(ligandReceptorData.getGenes({ name: 'l3' }).length).toEqual(0); 90 | expect(ligandReceptorData.getGenes({ class: 'ligand' }).length).toEqual(2); 91 | }); 92 | 93 | $httpBackend.flush(); 94 | }); 95 | }); 96 | 97 | describe('getPairs', function() { 98 | it('should return correct results', function () { 99 | 100 | ligandReceptorData.load().then(function() { 101 | expect(ligandReceptorData.getPairs({}).length).toEqual(2); 102 | expect(ligandReceptorData.getPairs({ name: 'l1-r1' }).length).toEqual(1); 103 | expect(ligandReceptorData.getPairs({ name: 'l2-r1' }).length).toEqual(0); 104 | expect(ligandReceptorData.getPairs({ ligand: {}, receptor: {} }).length).toEqual(2); 105 | expect(ligandReceptorData.getPairs({ ligand: { name: 'l1' } }).length).toEqual(1); 106 | expect(ligandReceptorData.getPairs({ ligand: { name: 'l1' }, receptor: { name: 'r1' } }).length).toEqual(1); 107 | expect(ligandReceptorData.getPairs({ ligand: { name: 'l1' }, receptor: { name: 'r2' } }).length).toEqual(0); 108 | }); 109 | 110 | $httpBackend.flush(); 111 | }); 112 | }); 113 | 114 | describe('getExpressionValues', function() { 115 | it('should return correct results', function () { 116 | 117 | ligandReceptorData.load().then(function() { 118 | expect(ligandReceptorData.getExpressionValues({}).length).toEqual(6); 119 | expect(ligandReceptorData.getExpressionValues({},2).length).toEqual(2); 120 | expect(ligandReceptorData.getExpressionValues({ligandMin: 10, receptorMin: 10}).length).toEqual(4); 121 | expect(ligandReceptorData.getExpressionValues({cell: {name:'c1'} }).length).toEqual(3); 122 | expect(ligandReceptorData.getExpressionValues({cell: {name:'c2'} }).length).toEqual(2); 123 | expect(ligandReceptorData.getExpressionValues({cell: {name:'c3'} }).length).toEqual(1); 124 | expect(ligandReceptorData.getExpressionValues({cell: {name:['c1','c2']} }).length).toEqual(5); 125 | }); 126 | 127 | $httpBackend.flush(); 128 | }); 129 | }); 130 | 131 | describe('getPathways', function() { 132 | it('should return correct results', function () { 133 | 134 | ligandReceptorData.load().then(function() { 135 | expect(ligandReceptorData.getPathways({}).length).toEqual(2); 136 | expect(ligandReceptorData.getPathways({ligandMin: 1, receptorMin: 1}).length).toEqual(4); 137 | expect(ligandReceptorData.getPathways({ligandMin: 1, receptorMin: 1},2).length).toEqual(2); 138 | }); 139 | 140 | $httpBackend.flush(); 141 | }); 142 | }); 143 | 144 | }); 145 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Ligand Receptor Connectome 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 |
37 | 38 | 39 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /app/components/ui/searchModalContent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 62 | 63 | 70 | 71 | 72 | 73 | 74 | 144 | 145 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /app/components/graph/graph-factory.js: -------------------------------------------------------------------------------- 1 | /* global _ */ 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | angular.module('lrSpaApp') 7 | 8 | .service('Graph', function($log, site) { 9 | 10 | var defaultSettings = {}; 11 | 12 | //TODO: 13 | // use better class contructor pattern? 14 | // Expose data through methods? (getNode, getFilteredNodes, getNeighbors?) 15 | // replace with sigma.js? 16 | 17 | return function Graph(settings) { 18 | 19 | settings = settings || defaultSettings; 20 | 21 | var graph = {}; 22 | 23 | var defaultData = { 24 | nodes: [], // -> nodesArray 25 | edges: [], // -> edgesArray 26 | 27 | nodesIndex: {}, 28 | edgesIndex: {}, // edgesIndex[nodeA.id][nodeB.id] 29 | 30 | inEdgesIndex: {}, 31 | outEdgesIndex: {}, 32 | 33 | // temp 34 | _nodes: [], // -> nodesArray 35 | _edges: [], // -> edgesArray 36 | _inEdgesIndex: {}, 37 | _outEdgesIndex: {}, 38 | 39 | edgeCount: 0, // get rid? 40 | ligandExtent: [0,100000], // get rid? 41 | receptorExtent: [0,100000], // get rid? 42 | 43 | selectedItems: [] 44 | }; 45 | 46 | graph.clear = function clear() { 47 | graph.data = defaultData; 48 | }; 49 | 50 | graph.clear(); 51 | 52 | graph.Node = function Node(id, name, type) { 53 | if (id) {this.id = id;} 54 | if (name) {this.name = name;} 55 | if (type) {this.type = type;} 56 | 57 | this.values = [0,0]; 58 | this.value = 0; 59 | this.lout = []; // todo: remove 60 | this.lin = []; 61 | 62 | this._expr = []; // rename these 63 | this._ligands = []; 64 | this._receptors = []; 65 | }; 66 | 67 | graph.addNode = function addNode(node) { 68 | if (typeof node !== 'object' || arguments.length !== 1) { 69 | $log.error('addNode: Wrong arguments.'); 70 | } 71 | 72 | //if (!node.ticked) { return; } 73 | 74 | var id = node.id; 75 | 76 | graph.data.nodes.push(node); 77 | graph.data.nodesIndex[id] = node; 78 | 79 | graph.data.edgesIndex[id] = {}; 80 | 81 | graph.data.inEdgesIndex[id] = []; 82 | graph.data.outEdgesIndex[id] = []; 83 | graph.data._inEdgesIndex[id] = []; 84 | graph.data._outEdgesIndex[id] = []; 85 | }; 86 | 87 | graph.Edge = function Edge(src,tgt,name) { 88 | name = name || src.name+'->'+tgt.name; 89 | return { 90 | source: src, 91 | target: tgt, 92 | value: 0, 93 | name: name, 94 | values: [0, 0] // remove these 95 | }; 96 | }; 97 | 98 | graph.addEdge = function addEdge(edge) { 99 | if (arguments.length !== 1 || typeof edge !== 'object') { 100 | $log.error('addNode: Wrong arguments.'); 101 | } 102 | 103 | edge.ticked = edge.source.ticked && edge.target.ticked; 104 | 105 | if (edge.ticked) { 106 | graph.data.edges.push(edge); 107 | } 108 | 109 | var src = edge.source.id; 110 | var tgt = edge.target.id; 111 | 112 | if (edge.source.ticked) { // todo: push sorted 113 | graph.data.edgesIndex[src] = graph.data.edgesIndex[src] || {}; 114 | graph.data.edgesIndex[src][tgt] = edge; 115 | 116 | graph.data.outEdgesIndex[src] = graph.data.outEdgesIndex[src] || []; 117 | graph.data.outEdgesIndex[src].push(edge); 118 | if (edge.ticked) { 119 | graph.data._outEdgesIndex[src].push(edge); 120 | } 121 | } 122 | 123 | if (edge.target.ticked) { 124 | graph.data.inEdgesIndex[tgt] = graph.data.inEdgesIndex[tgt] || []; 125 | graph.data.inEdgesIndex[tgt].push(edge); 126 | if (edge.ticked) { 127 | graph.data._inEdgesIndex[tgt].push(edge); 128 | } 129 | } 130 | 131 | }; 132 | 133 | function __getDATA() { 134 | var _json = { 135 | 136 | }; 137 | 138 | _json.nodes = graph.data._nodes.map(function(node, i) { 139 | 140 | var _n = { 141 | data: { 142 | id: i, 143 | name: node.name, 144 | type: node.type, 145 | class: node.class, 146 | value: String(node.value) 147 | }, 148 | position: { 149 | x: node.x, 150 | y: node.y 151 | } 152 | }; 153 | 154 | angular.extend(_n.data, node.meta); 155 | 156 | return _n; 157 | }); 158 | 159 | _json.edges = graph.data._edges.map(function(edge, i) { 160 | return { 161 | data: { 162 | id: i, 163 | name: edge.name, 164 | interaction: edge.type, 165 | source: graph.data._nodes.indexOf(edge.source), 166 | target: graph.data._nodes.indexOf(edge.target), 167 | value: String(edge.value) 168 | } 169 | }; 170 | }); 171 | 172 | return _json; 173 | } 174 | 175 | graph.getJSON = function _getJSON() { 176 | var _json = { 177 | 'format_version' : '1.0', 178 | 'generated_by' : [site.name,site.version].join('-'), 179 | 'target_cytoscapejs_version' : '~2.1', 180 | data: {}, 181 | elements: __getDATA() 182 | }; 183 | 184 | _json.elements.nodes.forEach(function(d) { 185 | d.data.id = String(d.data.id); 186 | }); 187 | 188 | _json.elements.edges.forEach(function(d) { 189 | d.data.id = String(d.data.id); 190 | d.data.source = String(d.data.source); 191 | d.data.target = String(d.data.target); 192 | }); 193 | 194 | return JSON.stringify(_json); 195 | }; 196 | 197 | graph.getGML = function __getGML() { 198 | 199 | function quote(str) { 200 | return '"'+str+'"'; 201 | } 202 | 203 | function indent(n, p) { 204 | n = n || 2; 205 | p = p || ' '; 206 | var pp = strRepeat(p, n); 207 | return function(s) { 208 | return pp+s; 209 | }; 210 | } 211 | 212 | function strRepeat(str, qty){ 213 | var result = ''; 214 | while (qty > 0) { 215 | result += str; 216 | qty--; 217 | } 218 | return result; 219 | } 220 | 221 | function convert(obj, k) { 222 | if (_.isString(obj)) {return [k,quote(obj)].join(' ');} 223 | if (_.isArray(obj)) {return [k,quote(String(obj))].join(' ');} 224 | if (_.isObject(obj)) { 225 | var e = _.map(obj, convert); 226 | e.unshift(k,'['); 227 | e.push(']'); 228 | return e.join(' '); 229 | } 230 | return [k,String(obj)].join(' '); 231 | } 232 | 233 | var _data = __getDATA(); 234 | 235 | var _gml = []; 236 | _gml.push('graph ['); 237 | 238 | _data.nodes.forEach(function(d) { 239 | _gml.push(' node ['); 240 | var e = _.map(d.data, convert).map(indent(4)); 241 | _gml = _gml.concat(e); 242 | 243 | _gml.push(' graphics ['); 244 | 245 | _gml.push(indent(6)(convert(d.position, 'center'))); 246 | 247 | _gml.push(' ]'); 248 | 249 | _gml.push(' ]'); 250 | }); 251 | 252 | _data.edges.forEach(function(d) { 253 | _gml.push(' edge ['); 254 | var e = _.map(d.data, convert).map(indent(4)); //function(v,k) { return ' '+convert(v,k); } ); 255 | _gml = _gml.concat(e); 256 | _gml.push(' ]'); 257 | }); 258 | 259 | _gml.push(']'); 260 | 261 | return _gml.join('\n'); 262 | 263 | }; 264 | 265 | return graph; 266 | 267 | }; 268 | 269 | }); 270 | 271 | })(); 272 | -------------------------------------------------------------------------------- /app/components/ui/main.css: -------------------------------------------------------------------------------- 1 | /* Space out content a bit */ 2 | body { 3 | padding: 0px; 4 | margin: 0; 5 | } 6 | 7 | .marketing, 8 | .footer, 9 | .container-fluid { 10 | padding: 0px; 11 | margin: 0; 12 | } 13 | 14 | #header { 15 | height: 40px; 16 | position: absolute; 17 | padding: 5px 20px; 18 | top: 0; 19 | left: 0; 20 | right: 0; 21 | color: #777; 22 | border-top: 1px solid #e5e5e5; 23 | } 24 | 25 | .navbar-toggle { 26 | display: inline-block; 27 | float: left; 28 | padding: 5px; 29 | margin: 0; 30 | background-color: buttonface; 31 | } 32 | 33 | .icon-bar { 34 | background-color: red; 35 | } 36 | 37 | /* Custom page footer */ 38 | 39 | #ui { 40 | background-color: #eee; 41 | position: absolute; 42 | top: 0; 43 | left: 0; 44 | width: 100%; 45 | opacity: 0.9; 46 | } 47 | 48 | #ui .title, #ui p { 49 | padding: 0 5px; 50 | } 51 | 52 | #ui .title, #ui .title + p { 53 | text-align: center; 54 | } 55 | 56 | #ui .collapsed > a > .caret, #ui a.collapsed > .caret, .caret.collapsed { 57 | border: 1px solid red; 58 | border-left: 4px solid; 59 | border-right: 4px solid transparent; 60 | border-bottom: 4px solid transparent; 61 | border-top: 4px solid transparent; 62 | } 63 | 64 | #ui:hover { 65 | opacity: 1; 66 | } 67 | 68 | .panel-group .panel { 69 | overflow: visible; 70 | } 71 | 72 | #content { 73 | overflow: hidden; 74 | } 75 | 76 | #footer { 77 | height: 30px; 78 | position: absolute; 79 | bottom: 0; 80 | left: 0; 81 | right: 0; 82 | background-color: #f5f5f5; 83 | color: #777; 84 | border-top: 1px solid #e5e5e5; 85 | } 86 | 87 | 88 | /* Customize container */ 89 | 90 | @media (min-width: 768px) { 91 | .container { 92 | max-width: 730px; 93 | } 94 | } 95 | @media (min-width: 1200px) { 96 | .container { 97 | max-width: 1170px; 98 | } 99 | } 100 | .container-narrow > hr { 101 | margin: 30px 0; 102 | } 103 | 104 | /* Main marketing message and sign up button */ 105 | .jumbotron { 106 | text-align: center; 107 | border-bottom: 1px solid #e5e5e5; 108 | } 109 | .jumbotron .btn { 110 | font-size: 21px; 111 | padding: 14px 24px; 112 | } 113 | 114 | /* Supporting marketing content */ 115 | .marketing { 116 | margin: 40px 0; 117 | } 118 | .marketing p + h4 { 119 | margin-top: 28px; 120 | } 121 | 122 | .rotate { 123 | display: inline-block; 124 | } 125 | 126 | 127 | 128 | .snap-content { 129 | background-color: white; 130 | } 131 | 132 | #ui .panel-group .panel { 133 | border-radius: 0; 134 | background-color: transparent; 135 | border: 0; 136 | -webkit-box-shadow: none; 137 | box-shadow: none; 138 | } 139 | 140 | #ui .panel-heading { 141 | padding: 10px 5px; 142 | border: 0; 143 | border-radius: 0; 144 | background-color: buttonface; 145 | cursor: pointer; 146 | } 147 | 148 | #ui .panel-body { 149 | padding: 15px 5px; 150 | } 151 | 152 | #ui .panel .panel-title a:after { 153 | font-family: 'Glyphicons Halflings'; 154 | content: "\e114"; 155 | float: right; 156 | color: grey; 157 | font-size: 12px; 158 | margin: 5px 5px; 159 | } 160 | 161 | #ui .panel.collapsed .panel-title a:after { 162 | content: "\e080"; /* adjust as needed, taken from bootstrap.css */ 163 | } 164 | 165 | .btn-vert-block > button{ 166 | margin-bottom:5px; 167 | } 168 | 169 | .multiSelect.inlineBlock { 170 | display: inline; 171 | } 172 | 173 | .multiSelect .button { 174 | width: 100%; 175 | } 176 | 177 | .multiSelect .show { 178 | width: 320px; 179 | overflow-y: auto; 180 | height: 300px; 181 | } 182 | 183 | .panel-right { 184 | /* border: 2px solid green; */ 185 | padding: 0; 186 | position: absolute; 187 | top: 0px; 188 | bottom: auto; 189 | right: 0px; 190 | left: auto; 191 | /* width: 400px; */ 192 | max-height: 100%; 193 | z-index: 100; 194 | overflow-y: auto; 195 | overflow-x: clip; 196 | } 197 | 198 | .growl { 199 | right: auto; 200 | left: 10px; 201 | width: 400px; 202 | } 203 | 204 | .growl .alert { 205 | margin-bottom: 4px; 206 | } 207 | 208 | .new-tip { 209 | line-height: 1; 210 | padding: 12px; 211 | opacity: 0.7; 212 | border-radius: 4px; 213 | margin: 5px; 214 | } 215 | 216 | .new-tip h5 { 217 | font-weight: bold; 218 | } 219 | 220 | .position-fixed { 221 | position: fixed; 222 | } 223 | 224 | .pointer { 225 | cursor: pointer; 226 | } 227 | 228 | .new-tip:hover, .new-tip.hover { 229 | opacity: 1; 230 | } 231 | 232 | .animate-fade { 233 | 234 | } 235 | 236 | .chosen-container-multi { 237 | width: 100% !important; 238 | } 239 | 240 | .chosen-container-multi .chosen-choices li.search-field input[type="text"] { 241 | height: auto; 242 | } 243 | 244 | .chosen-choices .search-field:first-child, .chosen-choices .search-field:first-child input[type="text"] { 245 | width: 100% !important; 246 | } 247 | 248 | .animate-fade.ng-hide-add, .animate-fade.ng-hide-remove, 249 | .animate-fade.ng-enter, .animate-fade.ng-leave { 250 | -webkit-transition:all linear 0.5s; 251 | -moz-transition:all linear 0.5s; 252 | -o-transition:all linear 0.5s; 253 | transition:all linear 0.5s; 254 | /* display:block!important; */ 255 | } 256 | 257 | .animate-fade.ng-hide-add.ng-hide-add-active, 258 | .animate-fade.ng-hide-remove, 259 | .animate-fade.ng-enter, 260 | .animate-fade.ng-leave.ng-leave-active { 261 | opacity: 0; 262 | } 263 | 264 | .animate-fade.ng-hide-add, 265 | .animate-fade.ng-hide-remove.ng-hide-remove-active, 266 | .animate-fade.ng-leave, 267 | .animate-fade.ng-enter.ng-enter-active { 268 | opacity: 1; 269 | } 270 | 271 | .animate-thin { 272 | 273 | } 274 | 275 | .animate-thin.ng-hide-add, .animate-thin.ng-hide-remove, 276 | .animate-thin.ng-enter, .animate-thin.ng-leave { 277 | -webkit-transition:all linear 0.5s; 278 | -moz-transition:all linear 0.5s; 279 | -o-transition:all linear 0.5s; 280 | transition:all linear 0.5s; 281 | display:block!important; 282 | position: relative; 283 | } 284 | 285 | .animate-thin.ng-hide-add.ng-hide-add-active, 286 | .animate-thin.ng-hide-remove, 287 | .animate-thin.ng-enter, 288 | .animate-thin.ng-leave.ng-leave-active { 289 | left: 100%; 290 | } 291 | 292 | .animate-thin.ng-hide-add, 293 | .animate-thin.ng-hide-remove.ng-hide-remove-active, 294 | .animate-thin.ng-leave, 295 | .animate-thin.ng-enter.ng-enter-active { 296 | left: 0; 297 | } 298 | 299 | /* Multilevel menu */ 300 | 301 | #vis .dropdown-menu { 302 | min-width: 260px; 303 | z-index: 2000; 304 | } 305 | 306 | .dropdown-submenu { 307 | position: relative; 308 | } 309 | 310 | .dropdown-submenu>.dropdown-menu { 311 | top: 0; 312 | left: 100%; 313 | margin-top: -6px; 314 | margin-left: -1px; 315 | -webkit-border-radius: 0 6px 6px 6px; 316 | -moz-border-radius: 0 6px 6px; 317 | border-radius: 0 6px 6px 6px; 318 | } 319 | 320 | .dropdown-submenu:hover>.dropdown-menu { 321 | display: block; 322 | } 323 | 324 | .dropdown-submenu>a:after { 325 | display: block; 326 | content: " "; 327 | float: right; 328 | width: 0; 329 | height: 0; 330 | border-color: transparent; 331 | border-style: solid; 332 | border-width: 5px 0 5px 5px; 333 | border-left-color: #ccc; 334 | margin-top: 5px; 335 | margin-right: -10px; 336 | } 337 | 338 | .dropdown-submenu:hover>a:after { 339 | border-left-color: #fff; 340 | } 341 | 342 | .dropdown-submenu.pull-left { 343 | float: none; 344 | } 345 | 346 | .dropdown-submenu.pull-left>.dropdown-menu { 347 | left: -100%; 348 | margin-left: 10px; 349 | -webkit-border-radius: 6px 0 6px 6px; 350 | -moz-border-radius: 6px 0 6px 6px; 351 | border-radius: 6px 0 6px 6px; 352 | } 353 | 354 | .nav-tabs > li { 355 | cursor: pointer; 356 | } 357 | 358 | .btn-icon { 359 | padding: 0; 360 | background-color: none; 361 | color: inherit; 362 | } 363 | 364 | .icon-check, input[type=checkbox].glyphicon:before, input[type=checkbox].fa:before { 365 | visibility: visible; 366 | opacity:0.5; 367 | } 368 | 369 | .icon-check.icon-checked, input[type=checkbox].glyphicon:checked:before, input[type=checkbox].fa:checked:before { 370 | opacity:1; 371 | } 372 | 373 | input[type=checkbox].glyphicon,input[type=checkbox].fa{ 374 | visibility: hidden; 375 | } 376 | 377 | .icon-checked.glyphicon-eye-close:before, input[type=checkbox].glyphicon-eye-close:checked:before { 378 | content: "\e105"; 379 | } 380 | 381 | .icon-checked.fa-unlock:before, input[type=checkbox].fa-unlock:checked:before { 382 | content: "\f023"; 383 | } 384 | 385 | .dropdown, .modal-dialog { 386 | z-index: 2000; 387 | } 388 | 389 | .modal-content button.close { 390 | padding: 10px; 391 | } 392 | 393 | .modal-content .nav-tabs > li { 394 | margin: 10px 0px -1px 10px; 395 | } 396 | 397 | .ngHeaderButton { 398 | -moz-border-radius: 10%; 399 | -webkit-border-radius: 10%; 400 | border-radius: 10%; 401 | width: auto; 402 | height: auto; 403 | top: 2px; 404 | text-size: 10px; 405 | padding: 3px 5px; 406 | } 407 | 408 | .ngRow .locked { 409 | color: #aaa; 410 | } 411 | 412 | .select2-container { 413 | width: 100%; 414 | } 415 | 416 | .miniGrid { 417 | border: 0; 418 | background-color: none; 419 | font-size: x-small; 420 | height: 50px; 421 | margin-top: 5px; 422 | } 423 | 424 | .miniGrid .ngRow.even, .miniGrid .ngRow { 425 | background-color: inherit; 426 | } 427 | 428 | .miniGrid .hgCellText { 429 | height: auto; 430 | } 431 | 432 | .ngHeaderContainer { 433 | position: static; 434 | } 435 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2014-04-10 using generator-angular 0.8.0 2 | 'use strict'; 3 | 4 | // # Globbing 5 | // for performance reasons we're only matching one level down: 6 | // 'test/spec/{,*/}*.js' 7 | // use this if you want to recursively match all subfolders: 8 | // 'test/spec/**/*.js' 9 | 10 | module.exports = function (grunt) { 11 | 12 | // Load grunt tasks automatically 13 | require('load-grunt-tasks')(grunt); 14 | 15 | // Time how long tasks take. Can help when optimizing build times 16 | require('time-grunt')(grunt); 17 | 18 | // Define the configuration for all the tasks 19 | grunt.initConfig({ 20 | 21 | // Project settings 22 | build: { 23 | // configurable paths 24 | app: 'app', 25 | dist: 'dist', 26 | prod: '/cygdrive/w/home/harshbarger/ligand-receptor' 27 | }, 28 | 29 | // Watches files for changes and runs tasks based on the changed files 30 | watch: { 31 | bower: { 32 | files: ['bower.json'], 33 | tasks: ['bowerInstall'] 34 | }, 35 | js: { 36 | files: ['<%= build.app %>/components/**/*.js'], 37 | tasks: ['newer:jshint:all'], 38 | options: { 39 | livereload: '<%= connect.options.livereload %>' 40 | } 41 | }, 42 | styles: { 43 | files: ['<%= build.app %>/components/**/*.css'], 44 | tasks: ['newer:copy:styles','autoprefixer'] 45 | }, 46 | //styles: { 47 | // files: ['<%= build.app %>/styles/**/*.css'], 48 | // tasks: ['newer:copy:styles', 'newer:copy:dist','autoprefixer'] 49 | //}, 50 | //jsTest: { 51 | // files: ['test/spec/{,*/}*.js'], 52 | // tasks: ['newer:jshint:test', 'karma'] 53 | // }, 54 | gruntfile: { 55 | files: ['Gruntfile.js'] 56 | }, 57 | livereload: { 58 | options: { 59 | livereload: '<%= connect.options.livereload %>' 60 | }, 61 | files: [ 62 | '<%= build.app %>/{,*/}*.html', 63 | '<%= build.app %>/components/**/*.html', 64 | '.tmp/components/**/*.css', 65 | '<%= build.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 66 | ] 67 | } 68 | }, 69 | 70 | // The actual grunt server settings 71 | connect: { 72 | options: { 73 | port: 9000, 74 | // Change this to '0.0.0.0' to access the server from outside. 75 | hostname: 'localhost', 76 | livereload: 35730 77 | }, 78 | livereload: { 79 | options: { 80 | open: true, 81 | base: [ 82 | '.tmp', 83 | '<%= build.app %>' 84 | ] 85 | } 86 | }, 87 | test: { 88 | options: { 89 | port: 9001, 90 | base: [ 91 | '.tmp', 92 | 'test', 93 | '<%= build.app %>' 94 | ] 95 | } 96 | }, 97 | dist: { 98 | options: { 99 | base: '<%= build.dist %>' 100 | } 101 | } 102 | }, 103 | 104 | // Make sure code styles are up to par and there are no obvious mistakes 105 | jshint: { 106 | options: { 107 | jshintrc: '.jshintrc', 108 | reporter: require('jshint-stylish') 109 | }, 110 | all: [ 111 | 'Gruntfile.js', 112 | '<%= build.app %>/components/**/*.js', 113 | '!<%= build.app %>/components/tree/d3.hive.v0.js' 114 | ], 115 | test: { 116 | options: { 117 | jshintrc: 'test/.jshintrc' 118 | }, 119 | src: ['test/spec/{,*/}*.js'] 120 | } 121 | }, 122 | 123 | // Empties folders to start fresh 124 | clean: { 125 | dist: { 126 | files: [{ 127 | dot: true, 128 | src: [ 129 | '.tmp', 130 | '<%= build.dist %>/*', 131 | '!<%= build.dist %>/.git*' 132 | ] 133 | }] 134 | }, 135 | server: '.tmp' 136 | }, 137 | 138 | // Add vendor prefixed styles 139 | autoprefixer: { 140 | options: { 141 | browsers: ['last 1 version'] 142 | }, 143 | dist: { 144 | files: [{ 145 | expand: true, 146 | cwd: '.tmp/components/', 147 | src: '**/*.css', 148 | dest: '.tmp/components/' 149 | }] 150 | } 151 | }, 152 | 153 | // Automatically inject Bower components into the app 154 | bowerInstall: { 155 | app: { 156 | src: ['<%= build.app %>/index.html'], 157 | ignorePath: '<%= build.app %>/', 158 | //exclude: [ /chosen\.jquery\.js/ ] 159 | overrides: { 160 | chosen: { 161 | main: 'bower_components/chosen/chosen.jquery.js' 162 | } 163 | } 164 | } 165 | }, 166 | 167 | // Renames files for browser caching purposes 168 | rev: { 169 | dist: { 170 | files: { 171 | src: [ 172 | '<%= build.dist %>/scripts/{,*/}*.js', 173 | '<%= build.dist %>/styles/{,*/}*.css', 174 | '<%= build.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 175 | '<%= build.dist %>/styles/fonts/*' 176 | ] 177 | } 178 | } 179 | }, 180 | 181 | // Reads HTML for usemin blocks to enable smart builds that automatically 182 | // concat, minify and revision files. Creates configurations in memory so 183 | // additional tasks can operate on them 184 | useminPrepare: { 185 | html: '<%= build.app %>/index.html', 186 | options: { 187 | dest: '<%= build.dist %>', 188 | flow: { 189 | html: { 190 | steps: { 191 | js: ['concat', 'uglifyjs'], 192 | css: ['cssmin'] 193 | }, 194 | post: {} 195 | } 196 | } 197 | } 198 | }, 199 | 200 | // Performs rewrites based on rev and the useminPrepare configuration 201 | usemin: { 202 | html: ['<%= build.dist %>/{,*/}*.html'], 203 | css: ['<%= build.dist %>/styles/**/*.css'], 204 | options: { 205 | assetsDirs: ['<%= build.dist %>'] 206 | } 207 | }, 208 | 209 | // The following *-min tasks produce minified files in the dist folder 210 | cssmin: { 211 | options: { 212 | //root: '<%= build.app %>' 213 | } 214 | }, 215 | 216 | imagemin: { 217 | dist: { 218 | files: [{ 219 | expand: true, 220 | cwd: '<%= build.app %>/images', 221 | src: '{,*/}*.{png,jpg,jpeg,gif}', 222 | dest: '<%= build.dist %>/images' 223 | }] 224 | } 225 | }, 226 | 227 | svgmin: { 228 | dist: { 229 | files: [{ 230 | expand: true, 231 | cwd: '<%= build.app %>/images', 232 | src: '{,*/}*.svg', 233 | dest: '<%= build.dist %>/images' 234 | }] 235 | } 236 | }, 237 | 238 | htmlmin: { 239 | dist: { 240 | options: { 241 | collapseWhitespace: false, 242 | collapseBooleanAttributes: true, 243 | removeCommentsFromCDATA: true, 244 | removeOptionalTags: false 245 | }, 246 | files: [{ 247 | expand: true, 248 | cwd: '<%= build.dist %>', 249 | src: ['*.html', 'components/**/*.html'], 250 | dest: '<%= build.dist %>' 251 | }] 252 | } 253 | }, 254 | 255 | // ngmin tries to make the code safe for minification automatically by 256 | // using the Angular long form for dependency injection. It doesn't work on 257 | // things like resolve or inject so those have to be done manually. 258 | ngmin: { 259 | dist: { 260 | files: [{ 261 | expand: true, 262 | cwd: '.tmp/concat/scripts', 263 | src: '*.js', 264 | dest: '.tmp/concat/scripts' 265 | }] 266 | } 267 | }, 268 | 269 | // Replace Google CDN references 270 | cdnify: { 271 | dist: { 272 | html: ['<%= build.dist %>/*.html'] 273 | } 274 | }, 275 | 276 | // Copies remaining files to places other tasks can use 277 | copy: { 278 | dist: { 279 | files: [{ 280 | expand: true, 281 | dot: true, 282 | cwd: '<%= build.app %>', 283 | dest: '<%= build.dist %>', 284 | src: [ 285 | '*.{ico,png,txt}', 286 | '.htaccess', 287 | '*.html', 288 | 'views/{,*/}*.html', 289 | 'images/{,*/}*.{webp}', 290 | 'components/{,*/}*.html', 291 | 'data/*.txt', 292 | 'fonts/*' 293 | ] 294 | }, { 295 | expand: true, 296 | cwd: '.tmp/images', 297 | dest: '<%= build.dist %>/images', 298 | src: ['generated/*'] 299 | }, { 300 | expand: true, 301 | cwd: '<%= build.app %>/bower_components/chosen', 302 | dest: '<%= build.dist %>/styles', 303 | src: ['*.png'] 304 | }, { 305 | expand: true, 306 | cwd: '<%= build.app %>/bower_components/select2', 307 | dest: '<%= build.dist %>/styles', 308 | src: ['*.png','*.gif'] 309 | }] 310 | }, 311 | styles: { 312 | expand: true, 313 | cwd: '<%= build.app %>/components', 314 | dest: '.tmp/components', 315 | src: '**/*.css' 316 | }, 317 | fonts: { 318 | expand: true, 319 | cwd: '<%= build.app %>/bower_components/bootstrap/dist/', 320 | dest: '<%= build.dist %>', 321 | src: 'fonts/*.*' 322 | }, 323 | 'fonts-fa': { 324 | expand: true, 325 | cwd: '<%= build.app %>/bower_components/font-awesome/', 326 | dest: '<%= build.dist %>', 327 | src: 'fonts/*.*' 328 | } 329 | }, 330 | 331 | // Run some tasks in parallel to speed up the build process 332 | concurrent: { 333 | server: [ 334 | 'copy:styles' 335 | ], 336 | test: [ 337 | 'copy:styles' 338 | ], 339 | dist: [ 340 | 'copy:styles', 341 | 'copy:fonts', 342 | 'copy:fonts-fa', 343 | 'imagemin', 344 | 'svgmin' 345 | ] 346 | }, 347 | 348 | // By default, your `index.html`'s will take care of 349 | // minification. These next options are pre-configured if you do not wish 350 | // to use the Usemin blocks. 351 | // cssmin: { 352 | // dist: { 353 | // files: { 354 | // '<%= build.dist %>/styles/main.css': [ 355 | // '.tmp/styles/{,*/}*.css', 356 | // '<%= build.app %>/styles/{,*/}*.css' 357 | // ] 358 | // } 359 | // } 360 | // }, 361 | // uglify: { 362 | // dist: { 363 | // files: { 364 | // '<%= build.dist %>/scripts/scripts.js': [ 365 | // '<%= build.dist %>/scripts/scripts.js' 366 | // ] 367 | // } 368 | // } 369 | // }, 370 | // concat: { 371 | // dist: {} 372 | // }, 373 | 374 | // Test settings 375 | karma: { 376 | unit: { 377 | configFile: 'karma.conf.js', 378 | singleRun: true 379 | } 380 | }, 381 | 382 | // Rsync to 'production" server 383 | rsync: { 384 | options: { 385 | args: ['--verbose','--delete'], 386 | recursive: true, 387 | exclude: ['.git*'] 388 | }, 389 | prod: { 390 | options: { 391 | src: '<%= build.dist %>/', 392 | dest: '<%= build.prod %>' 393 | } 394 | } 395 | }, 396 | 397 | 'gh-pages': { 398 | options: { 399 | base: 'dist' 400 | }, 401 | src: ['**'] 402 | } 403 | 404 | }); 405 | 406 | 407 | grunt.registerTask('serve', function (target) { 408 | if (target === 'dist') { 409 | return grunt.task.run(['build', 'connect:dist:keepalive']); 410 | } 411 | 412 | grunt.task.run([ 413 | 'clean:server', 414 | 'bowerInstall', 415 | 'concurrent:server', 416 | 'autoprefixer', 417 | 'connect:livereload', 418 | 'watch' 419 | ]); 420 | }); 421 | 422 | grunt.registerTask('test', [ 423 | 'clean:server', 424 | 'concurrent:test', 425 | 'autoprefixer', 426 | 'connect:test', 427 | 'karma' 428 | ]); 429 | 430 | grunt.registerTask('build', [ 431 | 'clean:dist', 432 | 'bowerInstall', 433 | 'useminPrepare', 434 | 'concurrent:dist', 435 | 'autoprefixer', 436 | 'concat', 437 | 'ngmin', 438 | 'copy:dist', 439 | //'cdnify', 440 | 'cssmin', 441 | 'uglify', 442 | 'rev', 443 | 'usemin', 444 | 'htmlmin' 445 | ]); 446 | 447 | grunt.registerTask('deploy', [ 448 | 'build', 449 | 'gh-pages' 450 | ]); 451 | 452 | grunt.registerTask('default', [ 453 | 'jshint', 454 | 'test', 455 | 'build' 456 | ]); 457 | 458 | }; 459 | -------------------------------------------------------------------------------- /app/components/ui/bottom-panel.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | 22 | 35 | 36 | 47 | 48 | 59 | 60 | 70 | 71 | 81 | 82 | 94 | 95 | 102 | 103 | 110 | 111 | 117 | 118 | 123 | 124 | 146 | 147 | 148 | 149 | 150 | Cell types ({{selectedIds.cells.length}}/{{ (data.cells | filter : { locked: false }).length}}) 151 | 152 |
153 |
154 | 155 | Genes ({{selectedIds.genes.length}}/{{(data.genes | filter : { locked: false }).length}}) 156 |
157 |
158 | 159 | Pairs ({{selectedIds.pairs.length}}/{{(data.pairs | filter : { locked: false }).length}}) 160 |
161 |
162 | 163 | Options 164 |
165 |
166 |

Graph

167 |
168 |
    169 |
  • Show node labels 170 | 172 |
173 |
174 | 175 |
176 |
177 |

Edges

178 |
179 | 180 |
181 | Ligand TPM 182 |
> {{options.ligandFilter | number:0}} tpm
183 |
184 | 185 |
190 |
191 | 192 |

193 |

194 | Receptor TPM 195 |
> {{options.receptorFilter | number:0}} tpm
196 |
197 | 198 |
204 |
205 | 206 |

207 |

208 | Expression edge cutoff 209 |
top {{options.edgeRankFilter | percentage}}
210 |
211 | 212 |
217 |
218 | 219 |
220 |
221 |
222 |
223 | Reset options 224 |
225 |
226 |
227 |
228 | 229 | Help 230 |
231 |

Ligand Receptor Connectome {{site.version}}

232 |
233 |

A visual guide to FANTOM5 Ligand-Receptor interactions as described in Ramilowski et al. ‘A draft network of ligand-receptor mediated multicellular signaling in human’ 2015. 234 |
Site source code at Hypercubed/connectome.

235 |
236 | 237 |

238 | Entity and options panel - (bottom right) 239 |
Displays entities and options panel 240 |

241 |
    242 |
  • Tabs: switch between lists of cells, genes, and ligand receptor pairs
  • 243 |
  • Visibility Toggle buttons: ( left in each row) turn on/off cells and genes (ligand/receptors)
  • 244 |
  • ctrl-click and shift-click: select multiple nodes and change visibility as a group
  • 245 |
  • Search... button: (upper right of the data list) filters each lists by substring search
  • 246 |
247 | 248 |

249 | Information panel - (top right) 250 |
Displays information on current network and on selected nodes/edges 251 |

252 | 253 |

254 | Search feature - 255 |
Search for top expression values and/or top cell-to-cell communication paths 256 |

257 | 258 |

259 | Visualization style - 260 |
Switch between hive/force-direct plots 261 |

262 | 263 |

264 | Save feature - 265 |
Save current view as SVG, download data in JSON or GML format, bookmark this view for later 266 |

267 | 268 |

269 | Visualization 270 |

271 |
    272 |
  • Click: select/deselect a node (information appears in the information panel)
  • 273 |
  • Ctrl-click: select multiple nodes
  • 274 |
  • Shift-Click: select a node and neighbors
  • 275 |
  • Right click: additional options
  • 276 |
277 | 278 |
279 |
280 |
281 | 282 |
283 | 289 |
290 | -------------------------------------------------------------------------------- /app/components/ui/main.js: -------------------------------------------------------------------------------- 1 | /* global saveAs */ 2 | /* global _F */ 3 | /* global Base64 */ 4 | /* global ngGridFlexibleHeightPlugin */ 5 | 6 | (function() { 7 | 'use strict'; 8 | 9 | var _value = _F('value'); 10 | var _specificity = _F('specificity'); 11 | var _ticked = _F('ticked'); 12 | var _i = _F('i'); 13 | 14 | angular.module('lrSpaApp') 15 | 16 | .controller('ResetCtrl', function ($state,localStorageService) { 17 | localStorageService.clearAll(); 18 | $state.go('home.hive-graph'); 19 | }) 20 | 21 | .controller('MainCtrl', function ($scope, $rootScope, $log, $state, $filter, $templateCache, $timeout, $window, $location, growl, filterFilter, cfpLoadingBar, debounce, site, localStorageService, loadedData, ligandReceptorData, forceGraph, hiveGraph) { 22 | 23 | $rootScope.$on('$routeChangeError', function(event) { 24 | $log.warn(event); 25 | }); 26 | 27 | $scope.data = loadedData; 28 | 29 | $scope.max = Math.max; // TODO: check if still needed 30 | 31 | var defaultOptions = { 32 | showLabels: true, 33 | maxEdges: 100, 34 | ligandFilter: 10, 35 | receptorFilter: 10, 36 | ligandRankFilter: 1, 37 | receptorRankFilter: 1, 38 | edgeRankFilter: 1, 39 | }; 40 | 41 | var defaultIds = {pairs:[569],cells:[13,15,16,17,33,62,69,72,73],genes:[145,768]}; 42 | 43 | $scope.options = angular.copy(defaultOptions); 44 | $scope.selectedIds = angular.copy(defaultIds); 45 | 46 | // Options 47 | //localStorageService.bind($scope, 'options', angular.extend({}, defaultOptions)); 48 | //localStorageService.bind($scope, 'selectedIds', angular.extend({}, defaultIds)); 49 | 50 | //$scope.selected = {}; 51 | $scope.resetOptions = function() { 52 | $scope.options = angular.extend({}, defaultOptions); 53 | clearlocks(); 54 | }; 55 | 56 | function clearlocks() { 57 | loadedData.pairs.forEach(function(d) { 58 | d.locked = false; 59 | }); 60 | loadedData.genes.forEach(function(d) { 61 | d.locked = false; 62 | }); 63 | loadedData.cells.forEach(function(d) { 64 | d.locked = false; 65 | }); 66 | } 67 | 68 | $scope.resetVis = function() { 69 | $scope.options = angular.copy(defaultOptions); 70 | $scope.selectedIds = angular.copy(defaultIds); 71 | loadSelection(); 72 | }; 73 | 74 | $scope.clearVis = function() { 75 | $scope.selectedIds = {pairs:[],cells:[],genes:[]}; 76 | loadSelection(); 77 | }; 78 | 79 | var _fixed = _F('fixed'); 80 | 81 | $scope.updateSelection = function() { 82 | graphService.data.selectedItems = graphService.data.nodes.filter(_fixed); 83 | }; 84 | 85 | $scope.rightClick = function(e) { // TODO: check if still needed 86 | if (e.target.__data__ && e.target.__data__.type) { 87 | $scope.clickedItem = e.target.__data__; 88 | } else { 89 | $scope.clickedItem = null; 90 | } 91 | }; 92 | 93 | // Graph service 94 | var graphService; // This should controlled by the directive 95 | 96 | $scope.state = $state.current; 97 | 98 | $scope.$watch('state.name', function(name) { // git rid of watchers 99 | $state.go(name); 100 | if (graphService) {graphService.clear();} 101 | graphService = (name === 'home.hive-graph') ? hiveGraph : forceGraph; 102 | $scope.graph = graphService; 103 | $scope.graphData = graphService.data; 104 | updateNetwork(); 105 | }); 106 | 107 | // TODO: move to saveModel controller 108 | $scope.saveJson = function() { // TODO: a directive/service? 109 | var txt = graphService.graph.getJSON(); 110 | var blob = new Blob([txt], { type: 'text/json' }); 111 | saveAs(blob, 'lr-graph.json'); 112 | }; 113 | 114 | $scope.saveGml = function() { // TODO: a directive/service? 115 | var txt = graphService.graph.getGML(); 116 | var blob = new Blob([txt], { type: 'text/gml' }); 117 | saveAs(blob, 'lr-graph.gml'); 118 | }; 119 | 120 | function _ticked2(arr) { // must be 121 | return function(d) { 122 | if (d.locked) { return false; } 123 | d.fixed = false; 124 | d.ticked = arr.indexOf(d.i) > -1; 125 | return d.ticked; 126 | }; 127 | } 128 | 129 | function loadSelection() { 130 | 131 | $log.debug('load from local storage'); 132 | 133 | $scope.data.pairs.filter(_ticked2($scope.selectedIds.pairs)); 134 | $scope.data.cells.filter(_ticked2($scope.selectedIds.cells)); 135 | $scope.data.genes.filter(_ticked2($scope.selectedIds.genes)); 136 | 137 | } 138 | 139 | $scope.revoveSelectedItem = function(index) { // TODO: check if still needed 140 | graphService.data.selectedItems[index].fixed = false; 141 | graphService.data.selectedItems.splice(index, 1); 142 | graphService.update(); 143 | }; 144 | 145 | var updateNetwork = debounce(function updateNetwork() { // This should be handeled by the directive 146 | $log.debug('update network'); 147 | 148 | if (graphService) { 149 | graphService.makeNetwork($scope.data, $scope.options); 150 | graphService.draw($scope.options); 151 | saveUndo(); 152 | } 153 | 154 | }); 155 | 156 | 157 | // TODO: move these to findModelCtrl 158 | $scope.showExpressionEdges = function _showExpressionEdges(_filter, max) { 159 | var filter = angular.copy(_filter); 160 | var acc = (filter.rank === 'specificity') ? _specificity : _value; 161 | 162 | if (filter.gene.class !== '') { 163 | delete filter.gene.id; 164 | } 165 | 166 | filter.gene = angular.extend({}, filter.gene, {locked: false}); 167 | filter.cell = angular.extend({}, filter.cell, {locked: false}); 168 | 169 | //console.log(filter); 170 | 171 | filter.ligandMin = $scope.options.ligandFilter; 172 | filter.receptorMin = $scope.options.receptorFilter; 173 | 174 | var start = new Date().getTime(); 175 | 176 | var edges; 177 | 178 | if (filter.gene.class === 'each') { 179 | var f = angular.copy(filter); 180 | f.gene.class = 'ligand'; 181 | var edges1 = ligandReceptorData.getExpressionValues(f, max, acc); 182 | 183 | f.gene.class = 'receptor'; 184 | var edges2 = ligandReceptorData.getExpressionValues(f, max, acc); 185 | 186 | edges = edges1.concat(edges2); 187 | } else { 188 | edges = ligandReceptorData.getExpressionValues(filter, max, acc); 189 | } 190 | 191 | $log.debug('found',edges.length,'expression edges'); 192 | 193 | if (edges.length < 1) { 194 | growl.addWarnMessage('No expression edges match search criteria and expression thresholds.'); 195 | } else { 196 | //growl.addSuccessMessage('Found '+edges.length+' expression edges'); 197 | 198 | edges.forEach(function(d) { 199 | d.gene.ticked = true; 200 | d.cell.ticked = true; 201 | }); 202 | 203 | loadedData.pairs.forEach(function(pair) { 204 | pair.ticked = !pair.locked && pair.ligand.ticked && pair.receptor.ticked; 205 | }); 206 | } 207 | 208 | var time = (new Date().getTime()) - start; 209 | $log.debug('Execution time:', time/1000, 's'); 210 | 211 | }; 212 | 213 | $scope.showPaths = function _showPaths(_filter, max) { 214 | var filter = angular.copy(_filter); 215 | 216 | var acc = (filter.rank === 'specificity') ? _specificity : _value; 217 | 218 | filter.pair = angular.extend({}, filter.pair, {locked: false}); 219 | filter.pair.ligand = angular.extend({}, filter.pair.ligand, {locked: false}); 220 | filter.pair.receptor = angular.extend({}, filter.pair.receptor, {locked: false}); 221 | filter.source = angular.extend({}, filter.source, {locked: false}); 222 | filter.target = angular.extend({}, filter.target, {locked: false}); 223 | 224 | cfpLoadingBar.start(); 225 | var start = Date.now(); 226 | 227 | $timeout(function() { 228 | 229 | filter.ligandMin = $scope.options.ligandFilter; 230 | filter.receptorMin = $scope.options.receptorFilter; 231 | 232 | var paths; 233 | 234 | if (filter.direction === 'each' && !angular.equals(filter.target, filter.source)) { 235 | $log.debug('Bi-directional search'); 236 | 237 | var f = angular.copy(filter); // do I need this, already a copy? 238 | f.direction = 'AB'; 239 | 240 | var paths1 = ligandReceptorData.getPathways(f, max, acc); 241 | 242 | f = angular.copy(filter); 243 | f.direction = 'BA'; 244 | f.source = filter.target; 245 | f.target = filter.source; 246 | 247 | var paths2 = ligandReceptorData.getPathways(f, max, acc); 248 | 249 | paths = paths1.concat(paths2); 250 | } else { 251 | paths = ligandReceptorData.getPathways(filter, max, acc); 252 | } 253 | 254 | $log.debug('found',paths.length,'expression edges'); 255 | 256 | if (paths.length < 1) { 257 | growl.addWarnMessage('No pathways match search criteria and expression thresholds.'); 258 | } else { 259 | paths.forEach(function(d) { 260 | d.pair.ticked = true; 261 | d.source.ticked = true; 262 | d.ligand.ticked = true; 263 | d.receptor.ticked = true; 264 | d.target.ticked = true; 265 | }); 266 | } 267 | 268 | var time = Date.now() - start; 269 | $log.debug('Execution time:', time/1000, 's'); 270 | 271 | cfpLoadingBar.complete(); 272 | }); 273 | 274 | }; 275 | 276 | $scope.hide = function(arr) { // TODO: check if still needed 277 | if (!angular.isArray(arr)) { arr = [arr]; } 278 | arr.forEach(function(d) { 279 | if (d.type === 'gene' || d.type === 'sample') { 280 | d.ticked = false; 281 | } 282 | }); 283 | }; 284 | 285 | $scope.pathFilter = { // TODO: check if still needed 286 | source: {}, 287 | target: {} 288 | }; 289 | 290 | // TODO: create a state undo service 291 | $scope.undoStack = []; 292 | $scope.undoIndex = -1; 293 | 294 | function saveUndo() { 295 | var b = save(); 296 | if ($scope.undoStack[$scope.undoIndex] !== b) { 297 | $scope.undoStack.push(b); 298 | $scope.undoIndex = $scope.undoStack.length-1; 299 | } 300 | } 301 | 302 | $scope.undo = function() { 303 | $scope.undoIndex--; 304 | var b = $scope.undoStack[$scope.undoIndex]; 305 | load(b); 306 | }; 307 | 308 | $scope.redo = function() { 309 | $scope.undoIndex++; 310 | var b = $scope.undoStack[$scope.undoIndex]; 311 | load(b); 312 | }; 313 | 314 | function save() { 315 | var obj = { 316 | i: $scope.selectedIds, 317 | o: $scope.options, 318 | v: site.apiVersion 319 | }; 320 | var json = JSON.stringify(obj); 321 | return Base64.encode(json); 322 | } 323 | 324 | function load(bin) { 325 | var json = Base64.decode(bin); 326 | var obj = JSON.parse(json); 327 | 328 | if (obj.v !== site.apiVersion) { 329 | $log.warn('Possible version issue'); 330 | } 331 | 332 | $log.debug('load',obj); 333 | 334 | $scope.selectedIds = obj.i; 335 | $scope.options = obj.o; 336 | 337 | loadSelection(); 338 | updateNetwork(); 339 | } 340 | 341 | //$scope.loadSave = function() {// TODO: check if still needed 342 | // var ret = $window.prompt('Copy this url to your clipboard to save the current state.', $scope.getSaveUrl()); 343 | //}; 344 | 345 | $scope.getSaveUrl = function() { 346 | return $location.absUrl()+'?save='+save(); 347 | }; 348 | 349 | // Start here 350 | clearlocks(); 351 | 352 | if ($location.search().save) { 353 | var b = $location.search().save; 354 | load(b); 355 | $location.search('save',null); 356 | } else { 357 | loadSelection(); 358 | updateNetwork(); 359 | } 360 | 361 | ['cells','pairs','genes'].forEach(function setSelectionWatch(key) { 362 | var arr = $scope.data[key]; 363 | var watchFn = function () { 364 | return arr.map(_ticked); 365 | }; 366 | var callBack = function() { 367 | 368 | //console.log('dataChanged',key); 369 | 370 | $scope.selectedIds[key] = arr.filter(_ticked).map(_i); 371 | updateNetwork(); 372 | }; 373 | $scope.$watch(watchFn, callBack,true); // git rid of watchers 374 | }); 375 | 376 | $scope.$watchCollection('options', updateNetwork); // git rid of watchers 377 | 378 | /*jshint -W055 */ 379 | $scope.ngGridPlugins = [new ngGridFlexibleHeightPlugin()]; 380 | /*jshint +W055 */ 381 | 382 | }); 383 | 384 | })(); 385 | -------------------------------------------------------------------------------- /app/components/data/ligandReceptorData-service.js: -------------------------------------------------------------------------------- 1 | /* global d3 */ 2 | /* global _F */ 3 | 4 | (function() { 5 | 'use strict'; 6 | 7 | var _value = _F('value'); 8 | var _i0 = _F(0); 9 | 10 | angular.module('lrSpaApp') 11 | 12 | .constant('files', { 13 | expression: 'data/LR.expr.txt', 14 | pairs: 'data/LR.pairs.txt', 15 | genes: 'data/LR.genes.txt', 16 | ontology: 'data/ontology.txt' 17 | }) 18 | 19 | .service('ligandReceptorData', function($q, $log,$http,$timeout,dsv,files) { 20 | 21 | var service = {}; 22 | 23 | var cache = false; 24 | 25 | service.data = { 26 | expr: [], 27 | pairs: [], 28 | cells: [], 29 | genes: [], 30 | ontology: [] 31 | }; 32 | 33 | service.load = function load() { 34 | 35 | function _loadPairs(filename) { 36 | return dsv.tsv.get(filename, {cache: cache}, function(d,i) { 37 | return { 38 | i: i, 39 | id: d.Ligand+'_'+d.Receptor, 40 | name: d.Ligand+'-'+d.Receptor, 41 | Ligand: d.Ligand, 42 | Receptor: d.Receptor, 43 | ligandId: d.Ligand+'.ligand', 44 | receptorId: d.Receptor+'.receptor', 45 | Source: d.Source, 46 | Evidence: d.Evidence, 47 | PMIDs: d.PMIDs, 48 | Databases: d.Databases.split(',').join(', ') 49 | }; 50 | }) 51 | .error(function(data, status, headers, config) { 52 | $log.warn('Error',data, status, headers, config); 53 | }) 54 | .success(function(data) { 55 | $log.debug('Pairs loaded:',data.length); 56 | }) 57 | .then(function(res) { 58 | return res.data; 59 | }); 60 | } 61 | 62 | function _loadExpression(filename) { 63 | return dsv.tsv.getRows(filename, {cache: cache}, function(row, i) { 64 | 65 | if (i === 0) { return row; } 66 | return row.map(function(e,i) { 67 | return i === 0 ? e : +e; 68 | }); 69 | }) 70 | .error(function(data, status, headers, config) { 71 | $log.warn('Error',data, status, headers, config); 72 | }) 73 | .success(function(data) { 74 | $log.debug('Expression rows:', data.length); 75 | }) 76 | .then(function(res) { 77 | return res.data; 78 | }); 79 | } 80 | 81 | function _loadGenes(filename) { 82 | return dsv.tsv.get(filename, {cache: cache}, function(d) { 83 | /*jshint camelcase: false */ 84 | return { 85 | name: d.ApprovedSymbol, 86 | description: d.ApprovedName, 87 | class: d.Class.toLowerCase(), 88 | id: d.ApprovedSymbol+'.'+d.Class.toLowerCase(), 89 | age: d.Age, 90 | taxon: d.Taxon, 91 | consensus: d.Consensus_Call, 92 | type: 'gene', 93 | hgncid: d.HGNCID, 94 | uniprotid: d.UniProtID 95 | }; 96 | }) 97 | /*jshint camelcase: true */ 98 | .error(function(data, status, headers, config) { 99 | $log.warn('Error',data, status, headers, config); 100 | }) 101 | .success(function(data) { 102 | $log.debug('Genes loaded:', data.length); 103 | }) 104 | .then(function(res) { 105 | return res.data; 106 | }); 107 | } 108 | 109 | function _loadOntology(filename) { 110 | return dsv.tsv.get(filename, {cache: cache}) 111 | .error(function(data, status, headers, config) { 112 | $log.warn('Error',data, status, headers, config); 113 | }) 114 | .then(function(res) { 115 | 116 | var _ontology = {}; 117 | 118 | res.data.forEach(function(_item) { 119 | _ontology[_item.Cell] = _item.Ontology; 120 | }); 121 | 122 | return _ontology; 123 | }); 124 | } 125 | 126 | return $q.all([_loadPairs(files.pairs), _loadExpression(files.expression), _loadOntology(files.ontology), _loadGenes(files.genes)]) 127 | .then(function(data) { 128 | 129 | service.data.pairs = data[0]; 130 | var _expr = service.data.expr = data[1]; 131 | var _ontology = data[2]; 132 | service.data.genes = data[3]; 133 | 134 | //console.log(data); 135 | 136 | // get samples from expression table 137 | service.data.cells = _expr[0].slice(1).map(function(d,i) { 138 | 139 | var _cell = { 140 | name: d, 141 | id: String(i), // better name? 142 | i: i, 143 | type: 'sample' 144 | }; 145 | 146 | var _o = _ontology[d]; 147 | if (_o) { 148 | _cell.meta = _cell.meta || {}; 149 | _cell.meta.Ontology = _o; 150 | } 151 | 152 | return _cell; 153 | }); 154 | 155 | $log.debug('Samples loaded:', service.data.cells.length); 156 | $log.debug('Done loading'); 157 | 158 | /* function matchKeys(meta, match) { // Do this on load 159 | var keys = d3.keys(meta); 160 | var values = {}; 161 | 162 | keys.forEach(function(k) { 163 | if (k.match(match)) { 164 | values[k.replace(match,'').toLowerCase()] = meta[k]; 165 | } 166 | }); 167 | 168 | return values; 169 | } */ 170 | 171 | // Get index for each gene in expression table 172 | var _genesIndecies = _expr.slice(1).map(_i0); 173 | service.data.genes = service.data.genes.map(function(gene) { 174 | var i = _genesIndecies.indexOf(gene.name); 175 | gene.i = i; 176 | 177 | if (i > -1) { 178 | gene.median = d3.median(_expr[gene.i + 1].slice(1)); 179 | } else { 180 | gene.median = 0; 181 | } 182 | 183 | return gene; 184 | }); 185 | 186 | /* service.data.genes = _expr.slice(1).map(function(row, i) { // TODO: generate one gene file 187 | return { 188 | name: row[0], 189 | id: row[0], 190 | //id: i, // todo: get rid of this 191 | i: i, 192 | pairs: [], // todo: get rid of this 193 | type: 'gene', 194 | class: 'unknown', 195 | description: '', 196 | _genes: [], // todo: get rid of this 197 | ligands: [], // todo: get rid of this 198 | receptors: [] 199 | }; 200 | }); */ 201 | 202 | // cross reference pairs 203 | service.data.pairs = service.data.pairs.filter(function(pair) { 204 | 205 | var _ligand, _receptor; 206 | 207 | service.data.genes.forEach(function(gene) { 208 | //if (i === 0) { 209 | // console.log(gene.id, pair.ligandId); 210 | //}; 211 | if (gene.id === pair.ligandId) { 212 | _ligand = gene; 213 | } else if (gene.id === pair.receptorId) { 214 | _receptor = gene; 215 | } 216 | }); 217 | 218 | if (!_ligand || !_receptor) { 219 | $log.warn('Ligand or receptor missing from expression table', pair.ligandId, pair.receptorId); 220 | pair.index = [-1,-1]; 221 | return false; 222 | } 223 | 224 | pair.index = [_ligand.i,_receptor.i]; 225 | pair.ligand = _ligand; 226 | pair.receptor = _receptor; 227 | 228 | if (_ligand.class !== 'ligand') { 229 | $log.warn('Class inconsistancy',_ligand.name); 230 | return false; 231 | } 232 | 233 | if(_receptor.class !== 'receptor') { 234 | $log.warn('Class inconsistancy',_receptor.name); 235 | return false; 236 | } 237 | 238 | //console.log(_receptor.class == 'receptor'); 239 | 240 | // cross reference 241 | //_ligand.class = 'ligand'; 242 | //_ligand._genes.push(_receptor.i); 243 | //_ligand.receptors.push({ i: _receptor.i }); 244 | //_ligand.meta = matchKeys(pair, 'Ligand.'); 245 | //_ligand.description = _ligand.meta.name; 246 | //delete _ligand.meta.name; 247 | 248 | //_receptor.class = 'receptor'; 249 | //_receptor._genes.push(_ligand.i); 250 | //_receptor.ligands.push({ i: _ligand.i }); 251 | ////_receptor.meta = matchKeys(pair, 'Receptor.'); 252 | //_receptor.description = _receptor.meta.name; 253 | //delete _receptor.meta.name; 254 | 255 | //console.log(pair); 256 | 257 | return true; 258 | }); 259 | 260 | return service.data; 261 | 262 | }); 263 | }; 264 | 265 | function _match(obj, text) { 266 | if (text === '') { return true; } 267 | 268 | var key; 269 | 270 | // if both are objects check each key 271 | if (obj && text && typeof obj === 'object' && typeof text === 'object' ) { 272 | if (angular.equals(obj, text)) { return true; } 273 | for (key in text) { 274 | if (!hasOwnProperty.call(obj, key) || !_match(obj[key], text[key])) { 275 | return false; 276 | } 277 | } 278 | return true; 279 | } 280 | 281 | // if array, check at least one match 282 | if (angular.isArray(text)) { 283 | if (text.length === 0) { return true; } 284 | for (key in text) { 285 | if (_match(obj, text[key])) { 286 | return true; 287 | } 288 | } 289 | return false; 290 | } 291 | 292 | if (typeof text === 'boolean') { 293 | return obj === text; 294 | } 295 | 296 | return ''+obj === ''+text; 297 | } 298 | 299 | service.getGenes = function _getGenes(geneFilter) { 300 | if (!geneFilter) { return service.data.genes; } 301 | 302 | return service.data.genes.filter(function(gene) { 303 | //console.log(geneFilter , gene); 304 | return _match(gene,geneFilter); 305 | }); 306 | }; 307 | 308 | service.getCells = function _getCells(cellFilter) { 309 | if (!cellFilter) { return service.data.cells; } 310 | return service.data.cells.filter(function(cell) { 311 | return _match(cell,cellFilter); 312 | }); 313 | }; 314 | 315 | service.getExpressionValues = function (filter, max, acc) { 316 | filter = filter || {}; 317 | acc = acc || _value; 318 | 319 | var ligandMin = filter.ligandMin || 0; 320 | var receptorMin = filter.receptorMin || 0; 321 | 322 | var edges = []; 323 | 324 | var matchedCells = service.getCells(filter.cell); 325 | var matchedGenes = service.getGenes(filter.gene); 326 | //console.log(matchedCells); 327 | 328 | var count = 0; 329 | 330 | matchedGenes.forEach(function(gene) { 331 | if (gene.i < 0) { return; } 332 | //if (gene.locked) { return false; } // TODO: don't do this here 333 | //if (filter.gene && !_match(gene,filter.gene)) { return false; } 334 | 335 | var min = Math.max(gene.class === 'ligand' ? ligandMin : receptorMin,0); 336 | 337 | matchedCells.forEach(function(cell) { 338 | 339 | if (cell.i < 0) { return; } 340 | //if (cell.locked) { return false; } 341 | //if (filter.cell && !_match(cell,filter.cell)) { return false; } 342 | 343 | var v = +service.data.expr[gene.i+1][cell.i+1]; 344 | 345 | if (v > min) { // todo: insertion sort 346 | 347 | count++; 348 | 349 | edges.push( // TODO: sorted push 350 | { 351 | gene: gene, 352 | cell: cell, 353 | value: v, 354 | specificity: (v+1)/(gene.median+1) 355 | }); 356 | 357 | if (edges.length > max) { 358 | edges = edges.sort(function(a,b){ return acc(b) - acc(a); }).slice(0,max); 359 | } 360 | 361 | } 362 | 363 | }); 364 | }); 365 | 366 | //$log.debug('found',edges.length, 'expression values', count); 367 | return edges; 368 | 369 | }; 370 | 371 | service.getPairs = function _getPairs(pairFilter) { 372 | if (!pairFilter) { return service.data.pairs; } 373 | return service.data.pairs.filter(function(pair) { 374 | return _match(pair,pairFilter); 375 | }); 376 | }; 377 | 378 | service.getPathways = function getPathways(filter, max, acc) { 379 | max = max || 10; 380 | acc = acc || _value; 381 | 382 | var paths = []; 383 | 384 | $log.debug('Calculating pathways'); 385 | 386 | var ligandMin = (filter.ligandMin !== undefined) ? filter.ligandMin : 10; 387 | var receptorMin = (filter.receptorMin !== undefined) ? filter.receptorMin : 10; 388 | 389 | var count = 0; 390 | 391 | var matchedPairs = service.getPairs(filter.pair); 392 | if (matchedPairs.length < 1) { return []; } 393 | 394 | var matchedSourceCells = service.getCells(filter.source); 395 | if (matchedSourceCells.length < 1) { return []; } 396 | 397 | var matchedTargetCells = service.getCells(filter.target); 398 | if (matchedTargetCells.length < 1) { return []; } 399 | 400 | matchedPairs.forEach(function (pair) { 401 | 402 | //if (pair.locked) { return false; } 403 | 404 | if (pair.ligand.i < 0 || pair.receptor.i < 0) { return; } 405 | 406 | //if (filter.pair && !_match(pair,filter.pair)) { return; } 407 | 408 | //if (pair.ligand.locked) { return false; } 409 | //if (pair.receptor.locked) { return false; } 410 | 411 | if (filter.ligand && !_match(pair.ligand,filter.ligand)) { return; } // TOD: are these needed? 412 | if (filter.receptor && !_match(pair.receptor,filter.receptor)) { return; } 413 | 414 | //console.log(pair.name); 415 | 416 | //var ligandEdges = service.getExpressionValues({ cell: filter.source, gene: pair.ligand }); 417 | //var receptorEdges = service.getExpressionValues({ cell: filter.target, gene: pair.receptor }); 418 | 419 | //console.log(ligandEdges, receptorEdges); 420 | 421 | matchedSourceCells.forEach(function(source) { 422 | //ligandEdges.forEach(function(ligandEdge) { 423 | //var source = ligandEdge.cell; 424 | 425 | //if (source.locked) { return false; } // Move these out 426 | 427 | var l = +service.data.expr[pair.ligand.i+1][source.i+1]; 428 | var ls = (l+1)/(pair.ligand.median+1); 429 | 430 | if (l <= ligandMin) { return; } 431 | 432 | //if (filter.source && !_match(source,filter.source)) { return; } 433 | //if (filter.cell && !_match(source,filter.cell)) { return; } 434 | 435 | //console.log(source.name); 436 | 437 | matchedTargetCells.forEach(function(target) { 438 | //receptorEdges.forEach(function(receptorEdge) { 439 | //var target = receptorEdge.cell; 440 | 441 | //if (target.locked) { return false; } 442 | 443 | var r = +service.data.expr[pair.receptor.i+1][target.i+1]; 444 | var rs = (r+1)/(pair.receptor.median+1); 445 | 446 | if (r <= receptorMin) { return; } 447 | 448 | //if (filter.target && !_match(target,filter.target)) { return; } 449 | //if (filter.cell && !_match(target,filter.cell)) { return; } 450 | 451 | //console.log(target.name); 452 | 453 | // todo: use insertion sort (fin position, if pos > max, don't push) 454 | paths.push({ 455 | pair: pair, 456 | source: source, 457 | ligand: pair.ligand, 458 | receptor: pair.receptor, 459 | target: target, 460 | ligandExpression: l, 461 | receptorExpression: r, 462 | value: l*r, 463 | specificity: ls*rs 464 | }); 465 | 466 | if (paths.length > max) { 467 | paths = paths.sort(function(a,b) { return acc(b) - acc(a); }).slice(0,max); 468 | } 469 | 470 | count++; 471 | 472 | }); 473 | }); 474 | 475 | }); 476 | 477 | $log.debug('found',paths.length,'paths out of',count); 478 | //console.log('found',paths.length,'paths out of',count); 479 | 480 | return paths; 481 | }; 482 | 483 | return service; 484 | 485 | }); 486 | 487 | })(); 488 | -------------------------------------------------------------------------------- /app/components/charts/force-controller.js: -------------------------------------------------------------------------------- 1 | /* global d3 */ 2 | /* global lrd3 */ 3 | /* global _F */ 4 | 5 | (function() { 6 | 'use strict'; 7 | 8 | angular.module('lrSpaApp') 9 | 10 | .service('forceGraph', function($log, $window, $rootScope, $timeout, Graph, debounce, growl, cfpLoadingBar) { // TODO: should be a directive 11 | 12 | var chart = new lrd3.charts.forceGraph(); 13 | var graph = new Graph(); 14 | 15 | //function setupChart() { 16 | //chart = force2Graph(); // TODO: look at options 17 | 18 | // Events 19 | var _fixed = _F('fixed'); 20 | 21 | chart.on('hover', debounce(function(d) { 22 | graph.data.hoverEvent = true; 23 | 24 | graph.data.selectedItems = graph.data.selectedItems.filter(_fixed); 25 | 26 | if (d && graph.data.selectedItems.indexOf(d) < 0) { 27 | graph.data.selectedItems.unshift(d); 28 | } 29 | 30 | })); 31 | 32 | chart.on('selectionChanged', debounce(function() { 33 | //console.log(graph.data.selectedItems); 34 | var nodes = graph.data.nodes.filter(_fixed); 35 | var edges = graph.data.edges.filter(_fixed); 36 | graph.data.selectedItems = nodes.concat(edges); 37 | })); 38 | 39 | // Accesors 40 | var _value = _F('value'); 41 | var _ticked = _F('ticked'); 42 | //var _F = function(key) { return function(d) {return d[key];}; }; 43 | 44 | //var type = _F('type'); 45 | var _valueComp = function(a,b) { return _value(b) - _value(a); }; 46 | var _valueFilter = function(d) { return d.type !== 'node' || d.value >= 0; }; 47 | //var typeFilter = function(type) { return function(d) {return d.type === type;}; }; 48 | 49 | var _value0 = function(d) { return d.values[0]; }; 50 | var _value1 = function(d) { return d.values[1]; }; 51 | //var gtZero = function(d) {return d>0;}; 52 | 53 | function _makeNodes(genes, cells) { 54 | 55 | graph.data.nodes = []; 56 | graph.data.nodesIndex = {}; 57 | 58 | cells.forEach(function(cell) { 59 | var _node = angular.extend(cell, new graph.Node()); 60 | 61 | _node.type = 'sample'; 62 | graph.addNode(_node); 63 | }); 64 | 65 | genes.forEach(function(gene) { 66 | var _node = angular.extend(gene, new graph.Node()); 67 | _node.type = 'gene'; 68 | 69 | graph.addNode(_node); 70 | }); 71 | 72 | } 73 | 74 | function _sortAndFilterNodes() { //TODO: DRY this!!! 75 | 76 | graph.data.nodeCount = graph.data.nodes.length; // do I need this? 77 | 78 | var nodes = graph.data._nodes.sort(_valueComp).filter(_valueFilter); // Sort and filter out zeros 79 | 80 | var rankedLigands = nodes // Ligand 81 | .map(_value0) 82 | //.filter(gtZero) 83 | .sort(d3.ascending); 84 | 85 | var rankedReceptors = nodes // Receptor 86 | .map(_value1) 87 | //.filter(gtZero) 88 | .sort(d3.ascending); 89 | 90 | graph.data.ligandExtent = d3.extent(rankedLigands); // TODO: Already ranked, don't need extent 91 | graph.data.receptorExtent = d3.extent(rankedReceptors); 92 | 93 | /* var filter0 = d3.quantile(rankedLigands, 1-options.ligandRankFilter); 94 | var filter1 = d3.quantile(rankedReceptors, 1-options.receptorRankFilter); 95 | 96 | filter0 = Math.max(filter0, 0) || 0; 97 | filter1 = Math.max(filter1, 0) || 0; 98 | 99 | //console.log(filter0,filter1); 100 | 101 | var filtered = nodes.filter(function(d) { 102 | //console.log(d); 103 | return ( d.type !== 'sample' || d.values[0] >= filter0 || d.values[1] >= filter1 ); 104 | }); */ 105 | 106 | //console.log(filtered.length); 107 | 108 | graph.data._nodes = nodes; //filtered; 109 | 110 | } 111 | 112 | //var MAXEDGES = 1000; 113 | 114 | var StopIteration = new Error('Maximum number of edges exceeded'); 115 | 116 | function _makeEdges(cells, genes, pairs, expr, options) { // TODO: better 117 | 118 | try { 119 | return __makeEdges(cells, genes, pairs, expr, options); 120 | } catch(e) { 121 | if(e !== StopIteration) { 122 | throw e; 123 | } else { 124 | growl.addErrorMessage(StopIteration.message); 125 | return []; 126 | } 127 | } 128 | } 129 | 130 | function __makeEdges(cells, genes, pairs, expr, options) { //selected nodes 131 | 132 | graph.data.edges = []; 133 | 134 | //expression edges 135 | cells.forEach(function(cell) { 136 | if (!cell.ticked) { return; } 137 | //var nodeExpr = []; 138 | 139 | genes.forEach(function(gene) { 140 | if (!gene.ticked) { return; } 141 | 142 | //console.log(gene.i, cell.i); 143 | var v = (gene.i > -1 && cell.i > -1) ? +expr[gene.i + 1][cell.i + 1] : 0; 144 | var min = (gene.class === 'receptor') ? options.receptorFilter : options.ligandFilter; 145 | min = Math.max(min,0); 146 | 147 | if (v > min) { 148 | var src, tgt; 149 | 150 | if (gene.class === 'receptor') { 151 | src = gene; 152 | tgt = cell; 153 | } else { 154 | src = cell; 155 | tgt = gene; 156 | } 157 | 158 | //console.log(data.edgesIndex[src.id][tgt.id]); 159 | var s = (v+1)/(gene.median+1); 160 | 161 | var _edge = new graph.Edge(graph.data.nodesIndex[src.id],graph.data.nodesIndex[tgt.id]); 162 | _edge.gene = gene; 163 | _edge.cell = cell; 164 | _edge.value = v; 165 | //_edge.i = gene.i; // remove 166 | _edge.id = ''+src.id+tgt.id; // remove {target, source}.id 167 | _edge.type = 'expression'; // remove 168 | _edge.class = gene.class; 169 | _edge._specificity = s; 170 | _edge.specificity = Math.log(s)/Math.log(10); 171 | 172 | graph.addEdge(_edge); 173 | //nodeExpr.push(_edge); 174 | } 175 | 176 | }); 177 | 178 | }); 179 | 180 | // sample-sample edges 181 | pairs.forEach(function addLinks(_pair) { 182 | if (!_pair.ticked) { return; } 183 | 184 | var ligandEdges = graph.data.inEdgesIndex[_pair.ligandId]; 185 | var receptorEdges = graph.data.outEdgesIndex[_pair.receptorId]; 186 | 187 | ligandEdges.forEach(function(ligand) { 188 | receptorEdges.forEach(function(receptor) { 189 | 190 | var value = ligand.value*receptor.value; 191 | if (value > 0 && ligand.value > options.receptorFilter && receptor.value > options.ligandFilter) { 192 | //console.log(ligand.value,receptor.value,value); 193 | 194 | //var _edge; 195 | 196 | //if (data.edgesIndex[ligand.source.id]) { 197 | //console.log(data.edgesIndex[ligand.source.id][receptor.target.id]); 198 | //} 199 | var src = ligand.source; 200 | var tgt = receptor.target; 201 | 202 | var _edge = 203 | graph.data.edgesIndex[src.id][tgt.id] || 204 | new graph.Edge(src,tgt); 205 | 206 | var _s = ligand._specificity*receptor._specificity; 207 | var s = Math.log(_s)/Math.log(10); 208 | 209 | if (!_edge._specificity) { 210 | _edge._specificity = _edge.specificity = 0; 211 | } 212 | 213 | _edge.type = 'sample-sample'; 214 | _edge.id = 215 | _edge.name = src.name + ' -> ' + tgt.name; 216 | _edge.id = ''+src.id+tgt.id; 217 | _edge.value += value; 218 | _edge._specificity += _s; 219 | _edge.specificity += s; 220 | 221 | _edge.pairs = _edge.pairs || []; 222 | _edge.pairs.push({ 223 | pair: _pair, 224 | value: value, 225 | specificity: s 226 | }); 227 | 228 | graph.addEdge(_edge); 229 | } 230 | //delete ligandEdges[i]; 231 | //delete receptorEdges[j]; 232 | }); 233 | //delete ligandEdges[i]; 234 | }); 235 | 236 | }); 237 | 238 | var _type = _F('type'); 239 | var _typeIsExpression = _type.eq('expression'); 240 | var _valueDesc = function(a,b) { return b.value - a.value; }; 241 | 242 | 243 | graph.data.nodes.forEach(function(node) { // todo: move 244 | if (!node.ticked) { return; } 245 | 246 | //var a = function(a,b) { return b.value - a.value; }; 247 | 248 | //console.log(data.edgesIndex[node.id]); 249 | 250 | //data.edgesIndex[node.id] = data.edgesIndex[node.id].sort(a); 251 | graph.data.outEdgesIndex[node.id] = graph.data.outEdgesIndex[node.id].sort(_valueDesc); 252 | graph.data.inEdgesIndex[node.id] = graph.data.inEdgesIndex[node.id].sort(_valueDesc); 253 | 254 | graph.data._outEdgesIndex[node.id] = graph.data._outEdgesIndex[node.id].sort(_valueDesc); 255 | graph.data._inEdgesIndex[node.id] = graph.data._inEdgesIndex[node.id].sort(_valueDesc); 256 | 257 | node.values[0] = d3.sum(graph.data._outEdgesIndex[node.id].filter(_typeIsExpression),_value); 258 | node.values[1] = d3.sum(graph.data._inEdgesIndex[node.id].filter(_typeIsExpression),_value); 259 | node.value = d3.sum(node.values); 260 | //console.log(node.id); 261 | }); 262 | 263 | } 264 | 265 | function _draw(options) { 266 | $log.debug('Drawing graph'); 267 | 268 | //if (!chart) { 269 | // setupChart(options); 270 | //} 271 | 272 | if (graph.data.nodes.length < 1) { 273 | _clear(); 274 | return; 275 | } 276 | 277 | //$timeout(function() { 278 | d3.select('#vis svg') 279 | .classed('labels',options.showLabels) 280 | .datum(graph.data) 281 | .call(chart); 282 | //}); 283 | 284 | } 285 | 286 | function _update() { 287 | if (chart.update) { 288 | $log.debug('Updating graph'); 289 | chart.update(); 290 | } 291 | } 292 | 293 | function _clear() { 294 | $log.debug('Clearing'); 295 | 296 | graph.data.nodes = []; // Todo: graph.clear() 297 | graph.data.edges = []; 298 | graph.data.nodesIndex = {}; 299 | graph.data.edgesIndex = {}; 300 | 301 | d3.selectAll('#vis svg g').remove(); 302 | } 303 | 304 | function _makeNetwork(_data, options) { // pairs, cells, expr, options 305 | 306 | if (!_data) {return;} 307 | 308 | var pairs = _data.pairs.filter(function(d) { return d.ticked; }); // remove? 309 | var cells = _data.cells.filter(function(d) { return d.ticked; }); 310 | 311 | var expr = _data.expr; 312 | var genes = _data.genes.filter(function(d) { return d.ticked; }); 313 | 314 | $log.debug('Constructing'); 315 | 316 | if (cells.length < 1 && genes.length < 1) { 317 | _clear(); 318 | 319 | growl.addWarnMessage('No cells or genes selected'); 320 | return; 321 | } 322 | 323 | cfpLoadingBar.start(); 324 | 325 | _makeNodes(_data.genes, _data.cells); 326 | _makeEdges(_data.cells, _data.genes, pairs, expr, options); 327 | 328 | graph.data.edges = graph.data.edges.filter(_F('type').eq('sample-sample')); 329 | 330 | //console.log(data.nodes); 331 | 332 | $log.debug('Total nodes: ', graph.data.nodes.length); 333 | $log.debug('Total Edges: ', graph.data.edges.length); 334 | 335 | graph.data.edgeCount = graph.data.edges.length; // needed? 336 | 337 | //_sortAndFilterEdges(options); 338 | 339 | cfpLoadingBar.inc(); 340 | 341 | graph.data._nodes = graph.data.nodes 342 | .filter(_ticked) 343 | .filter(_F('type').eq('sample')); // combine these -> _sortAndFilterNodes 344 | _sortAndFilterNodes(options); 345 | 346 | graph.data.edges.forEach(function(d) { // -> sort and filter edges 347 | d.ticked = false; 348 | }); 349 | 350 | graph.data._nodes.forEach(function(node) { 351 | graph.data.outEdgesIndex[node.id].forEach(function(d) { 352 | d.ticked = true; 353 | }); 354 | }); 355 | 356 | 357 | graph.data._edges = graph.data.edges 358 | .filter(_ticked); 359 | //.filter(_F('type').eq('sample-sample')); 360 | 361 | if (graph.data._edges.length > options.edgeRankFilter*graph.data._edges.length) { 362 | 363 | graph.data._edges = graph.data._edges 364 | .sort(_valueComp) 365 | .slice(0,options.edgeRankFilter*graph.data.edges.length); 366 | 367 | graph.data._nodes.forEach(function(node) { 368 | node.inDegree = node.outDegree = 0; 369 | }); 370 | 371 | graph.data._edges.forEach(function(edge) { 372 | edge.target.inDegree++; 373 | edge.source.outDegree++; 374 | }); 375 | 376 | graph.data._nodes = graph.data._nodes.filter(function(node) { 377 | return node.inDegree > 0 || node.outDegree > 0; 378 | }); 379 | 380 | } 381 | 382 | $log.debug('Filtered nodes: ', graph.data._nodes.length); 383 | $log.debug('Filtered edges: ', graph.data._edges.length); 384 | 385 | cfpLoadingBar.inc(); 386 | 387 | graph.data._nodes.forEach(function(d) { 388 | 389 | if (d.type === 'gene') { 390 | d.group = 'gene.'+d.class; 391 | } else { 392 | d.group = 'sample'; 393 | if (d.values[0] > 0) { d.class='ligand';} // Ligand only, lime green 394 | if (d.values[1] > 0) { d.class='receptor';} // Receptor only, Very light blue 395 | if (d.values[0] > 0 && d.values[1] > 0) {d.class='both';} // Both, Dark moderate magenta 396 | } 397 | //console.log(d.ntype); 398 | }); 399 | 400 | cfpLoadingBar.complete(); 401 | 402 | } 403 | 404 | /* function __getDATA() { // This is hive version, move to service 405 | var _json = { 406 | 407 | }; 408 | 409 | _json.nodes = graph.data.nodes.map(function(node, i) { 410 | 411 | var _n = { 412 | data: { 413 | id: i, 414 | name: node.name, 415 | type: node.type.split('.')[1] || 'sample', 416 | value: String(node.value), 417 | genes: node.genes 418 | }, 419 | position: { 420 | x: node.x, 421 | y: node.y 422 | } 423 | }; 424 | 425 | _.extend(_n.data, node.meta); 426 | 427 | return _n; 428 | }); 429 | 430 | _json.edges = graph.data.edges.map(function(edge) { 431 | return { 432 | data: { 433 | id: edge.index, 434 | name: edge.name, 435 | source: graph.data.nodes.indexOf(edge.source), 436 | target: graph.data.nodes.indexOf(edge.target), 437 | value: String(edge.value) 438 | } 439 | }; 440 | }); 441 | 442 | return _json; 443 | } 444 | 445 | function _getJSON() { // This is hive version, move to service 446 | var _json = { 447 | 'format_version' : '1.0', 448 | 'generated_by' : [name,version].join('-'), 449 | 'target_cytoscapejs_version' : '~2.1', 450 | data: {}, 451 | elements: __getDATA() 452 | }; 453 | 454 | _json.elements.nodes.forEach(function(d) { 455 | d.data.id = String(d.data.id); 456 | }); 457 | 458 | _json.elements.edges.forEach(function(d) { 459 | d.data.id = String(d.data.id); 460 | d.data.source = String(d.data.source); 461 | d.data.target = String(d.data.target); 462 | }); 463 | 464 | return JSON.stringify(_json); 465 | } 466 | 467 | function _getGML() { // This is hive version, move to service 468 | 469 | function quote(str) { 470 | return '"'+str+'"'; 471 | } 472 | 473 | function indent(n, p) { 474 | n = n || 2; 475 | p = p || ' '; 476 | var pp = strRepeat(p, n); 477 | return function(s) { 478 | return pp+s; 479 | }; 480 | } 481 | 482 | function strRepeat(str, qty){ 483 | var result = ''; 484 | while (qty > 0) { 485 | result += str; 486 | qty--; 487 | } 488 | return result; 489 | } 490 | 491 | function convert(obj, k) { 492 | if (_.isString(obj)) {return [k,quote(obj)].join(' ');} 493 | if (_.isArray(obj)) {return [k,quote(String(obj))].join(' ');} 494 | if (_.isObject(obj)) { 495 | var e = _.map(obj, convert); 496 | e.unshift(k,'['); 497 | e.push(']'); 498 | return e.join(' '); 499 | } 500 | return [k,String(obj)].join(' '); 501 | } 502 | 503 | var _data = __getDATA(); 504 | 505 | var _gml = []; 506 | _gml.push('graph ['); 507 | 508 | _data.nodes.forEach(function(d) { 509 | _gml.push(' node ['); 510 | var e = _.map(d.data, convert).map(indent(4)); 511 | _gml = _gml.concat(e); 512 | 513 | _gml.push(' graphics ['); 514 | 515 | _gml.push(indent(6)(convert(d.position, 'center'))); 516 | 517 | _gml.push(' ]'); 518 | 519 | _gml.push(' ]'); 520 | }); 521 | 522 | _data.edges.forEach(function(d) { 523 | _gml.push(' edge ['); 524 | var e = _.map(d.data, convert).map(indent(4)); //function(v,k) { return ' '+convert(v,k); } ); 525 | _gml = _gml.concat(e); 526 | _gml.push(' ]'); 527 | }); 528 | 529 | _gml.push(']'); 530 | 531 | return _gml.join('\n'); 532 | 533 | }*/ 534 | 535 | return { 536 | graph: graph, 537 | data: graph.data, 538 | chart: chart, 539 | update: debounce(_update, 30), 540 | makeNetwork: _makeNetwork, 541 | draw: _draw, 542 | clear: _clear//, 543 | //getJSON: _getJSON, 544 | //getGML: _getGML 545 | }; 546 | 547 | }); 548 | 549 | })(); 550 | -------------------------------------------------------------------------------- /app/components/charts/hive-chart.js: -------------------------------------------------------------------------------- 1 | /* global d3 */ 2 | /* global _F */ 3 | 4 | (function(root) { 5 | 'use strict'; 6 | 7 | function degrees(radians) { 8 | return radians / Math.PI * 180-90; 9 | } 10 | 11 | var hiveSymbol = function() { 12 | var type = d3.functor('circle'), size = 64; 13 | 14 | /* jshint -W014 */ 15 | function symbolCircle(size) { 16 | var r = Math.sqrt(size / 3.14159); 17 | return 'M0,' + r 18 | + 'A' + r + ',' + r + ' 0 1,1 0,' + (-r) 19 | + 'A' + r + ',' + r + ' 0 1,1 0,' + r 20 | + 'Z'; 21 | } 22 | 23 | function symbolSquare(size) { 24 | var r = Math.sqrt(size) / 2; 25 | return 'M' + -r + ',' + -r 26 | + 'L' + r + ',' + -r 27 | + ' ' + r + ',' + r 28 | + ' ' + -r + ',' + r 29 | + 'Z'; 30 | } 31 | 32 | function symbolV(size) { 33 | var r = Math.sqrt(size) / 2; 34 | return 'M' + -r + ',' + -r 35 | + 'L' + 0 + ',' + 0 36 | + ' ' + r + ',' + -r 37 | + ' ' + r + ',' + r 38 | + ' ' + -r + ',' + r 39 | + 'Z'; 40 | } 41 | 42 | function symbolA(size) { 43 | var r = Math.sqrt(size) / 2; 44 | return 'M' + -r + ',' + -r 45 | + 'L' + r + ',' + -r 46 | + ' ' + r + ',' + 0 47 | + ' ' + 0 + ',' + r 48 | + ' ' + -r + ',' + 0 49 | + 'Z'; 50 | } 51 | /* jshint +W014 */ 52 | 53 | var symbols = d3.map({ 54 | 'circle': symbolCircle, 55 | 'square': symbolSquare, 56 | 'A': symbolA, 57 | 'V': symbolV 58 | }); 59 | 60 | /* jshint -W040 */ 61 | function symbol(d, i) { 62 | return (symbols.get(type.call(this, d, i)) 63 | || symbolCircle) 64 | (size.call(this, d, i)); 65 | } 66 | /* jshint +W040 */ 67 | 68 | symbol.type = function(x) { 69 | if (!arguments.length) {return type;} 70 | type = d3.functor(x); 71 | return symbol; 72 | }; 73 | 74 | // size of symbol in square pixels 75 | symbol.size = function(x) { 76 | if (!arguments.length) {return size;} 77 | size = d3.functor(x); 78 | return symbol; 79 | }; 80 | 81 | return symbol; 82 | }; 83 | 84 | var hiveGraph = function() { 85 | var width = 500, height = 500; 86 | var margin = { top: 10, right: 60, bottom: 240, left: 60}; 87 | //var padding = { top: 60, right: 100, bottom: 60, left: 60}; 88 | 89 | function chart(selection) { 90 | selection.each(chart.draw); 91 | } 92 | 93 | // elements 94 | var nodes, links; 95 | 96 | // Private objects 97 | var zoom = d3.behavior.zoom(); 98 | 99 | // Scales 100 | var groups = ['gene.ligand','gene.receptor','sample']; 101 | var ncolor = d3.scale.ordinal() 102 | .domain(['ligand','both','receptor']) 103 | //.range(['#8c564b','yellow','#1f77b4']); 104 | .range(['#ed1940','yellow','#3349ff']); 105 | //.range(['#ed1940','#a650e2','#9999ff']); 106 | //.range(['#ed1940','#a650e2','#3349ff']); 107 | var slog = d3.scale.log().range([2,9]).clamp(true); // Maps value to normalized edge width 108 | var eopac = d3.scale.linear().range([0.2,0.8]).clamp(true); 109 | var rsize = d3.scale.linear().range([3, 12]).clamp(true); // Maps value to size 110 | var angle = d3.scale.ordinal().domain(groups).range([-Math.PI/4+Math.PI, Math.PI/4+Math.PI, 0]); // maps type to angle 111 | var radius = d3.scale.linear(); // maps position to radius 112 | 113 | var _y = {}; // maps index to position 114 | groups.forEach(function(d) { 115 | _y[d] = d3.scale.ordinal().rangePoints([0, 1]); 116 | }); 117 | 118 | // Value accessors 119 | //var _type = _F('type'); 120 | var _group = _F('group'); 121 | var _value = _F('value'); 122 | //var _name = _F('name'); 123 | var _id = _F('id'); 124 | var _class = _F('class'); 125 | var _fixed = _F('fixed'); 126 | var _edgeFixed = _F('source',_fixed).and(_F('target',_fixed)); 127 | var _hover = _F('hover'); 128 | 129 | var linkName = function(d) { return [d.source.name, d.name, d.target.name].join(':'); }; 130 | var nodeName = function(d) { // TODO: not this 131 | return d.name.split('.')[0]; 132 | }; 133 | 134 | // Range accesors 135 | var _angle = _F('group', angle); //function(d) { return angle(d.type); }; 136 | var _radius = function(d) { 137 | if (!_y[_group(d)]) { return 0; } 138 | 139 | return radius(_y[_group(d)](d._i)); 140 | }; 141 | var _ncolor = _F('class', ncolor); //function(d) { return ncolor(d.class); }; 142 | var _slog = _F(_value, slog); 143 | 144 | var dispatch = d3.dispatch('hover','selectionChanged','contextmenu'); 145 | 146 | chart.draw = function draw(graph) { 147 | 148 | var container = d3.select(this); 149 | 150 | width = parseInt(container.style('width')); 151 | height = parseInt(container.style('height')); 152 | 153 | var size = Math.min(height, width)/(1+Math.cos(Math.PI/3))/1.5; 154 | radius.range([size/10, size]); 155 | 156 | chart.update = function() { 157 | updateClasses(); 158 | }; 159 | 160 | chart.container = container; 161 | 162 | container 163 | .attr('width', width) 164 | .attr('height', height) 165 | ; 166 | 167 | // Ranges 168 | var _e = d3.extent(graph._edges, _value); // Edge values 169 | slog.domain(_e); 170 | eopac.domain(_e); 171 | 172 | var _n = d3.extent(graph._nodes, _value); // Node values 173 | rsize.domain(_n); 174 | 175 | var nodesByType = d3.nest() // Nest nodes by type 176 | .key(_group) 177 | .sortKeys(d3.ascending) 178 | .entries(graph._nodes); 179 | 180 | nodesByType.forEach(function(type) { // Setup domain for position range 181 | if (_y[type.key]) { 182 | _y[type.key].domain(d3.range(type.values.length)); 183 | 184 | type.values.forEach(function(node,i) { 185 | node._i = i; 186 | }); 187 | } 188 | }); 189 | 190 | function _labelAngle(d) { 191 | var a = -_angle(d)+Math.PI; 192 | a = (a+2*Math.PI) % (2*Math.PI); 193 | if (a > Math.PI/2 && a < 3*Math.PI/2) {return a;} 194 | return a+Math.PI/2; 195 | } 196 | 197 | var hiveLink = d3.hive.link() 198 | .angle(_angle) 199 | .radius(_radius); 200 | 201 | container.selectAll('defs').remove(); 202 | 203 | /* var markers = container 204 | .append('defs') 205 | .selectAll('marker').data(graph._edges).enter() 206 | .append('svg:marker') 207 | .attr({ 208 | class: 'Triangle', 209 | viewBox: '0 -5 10 10', 210 | refY: 0, 211 | refX: 20, 212 | markerWidth: 5, 213 | markerHeight: 5, 214 | 'stroke-width': 1, 215 | markerUnits: 'strokeWidth', 216 | orient: 'auto', 217 | id: function(d,i) { return 'arrow-'+i; } 218 | }); 219 | 220 | markers.append('svg:path') 221 | .attr('d', 'M0,-5L10,0L0,5'); */ 222 | 223 | var g = container.selectAll('.hiveGraph').data([1]); 224 | 225 | g.enter() 226 | .append('g') 227 | .attr('class', 'hiveGraph') 228 | .each(function() { // Only called once 229 | container.call(zoom.on('zoom', rescale)); 230 | zoom.translate([width/2,height/2+(height/2-size)]); 231 | zoom.event(container); 232 | }); 233 | 234 | container.on('click', function() { 235 | if (d3.event.defaultPrevented) {return;} 236 | d3.event.stopPropagation(); 237 | 238 | graph.nodes.forEach(function(d) { 239 | d.fixed = false; 240 | }); 241 | 242 | updateClasses(); 243 | dispatch.selectionChanged(); 244 | }); 245 | 246 | // rescale g 247 | function rescale() { 248 | var trans=d3.event.translate; 249 | var scale=d3.event.scale; 250 | 251 | g.attr('transform', 252 | 'translate(' + trans + ') scale(' + scale + ')'); 253 | } 254 | 255 | //g.selectAll('.axis').remove(); 256 | 257 | var axes = g.selectAll('.axis') 258 | .data(groups); 259 | 260 | axes.enter().append('line') 261 | .style({ 262 | stroke: '#000', 263 | 'stroke-width': '1.5px' 264 | }); 265 | 266 | axes 267 | .attr({ 268 | class: 'axis', 269 | transform: function(d) { return 'rotate(' + degrees(angle(d)) + ')'; }, 270 | x1: radius.range()[0], 271 | x2: radius.range()[1] 272 | }); 273 | 274 | //function sign(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; } 275 | 276 | function classNeighbors(node, direction, key, value) { 277 | if (arguments.length < 4) { value = true; } 278 | 279 | var _tgt = (direction <= 0) ? 'source' : 'target'; 280 | //var _dir = (direction <= 0) ? 'lin' : 'lout'; 281 | 282 | var edges = (direction <= 0) ? graph._inEdgesIndex[node.id] : graph._outEdgesIndex[node.id]; 283 | 284 | edges.forEach(function(d) { 285 | if(d) { 286 | d[key] = value; // class edge 287 | 288 | var t = d[_tgt]; 289 | t[key] = value; // class node 290 | 291 | if (t.type === 'gene') { 292 | classNeighbors(t, direction, key, value); 293 | } 294 | 295 | } 296 | }); 297 | } 298 | 299 | function mouseoverEdgeHighlight(d) { 300 | 301 | //var edge = d3.select(this); 302 | 303 | var tgt = d.target; 304 | var src = d.source; 305 | 306 | d.hover = tgt.hover = src.hover = true; 307 | 308 | if (tgt.type === 'gene') { 309 | classNeighbors(tgt, 3, 'hover'); 310 | } 311 | 312 | if (src.type === 'gene') { 313 | classNeighbors(src, -3, 'hover'); 314 | } 315 | 316 | chart.container.classed('hover',true); 317 | updateClasses(); 318 | 319 | dispatch.hover(d); 320 | } 321 | 322 | function mouseoverNodeHighlight(d) { 323 | 324 | //var node = d3.select(this); 325 | 326 | d.hover = true; 327 | 328 | classNeighbors(d, 3, 'hover'); 329 | classNeighbors(d, -3, 'hover'); 330 | 331 | chart.container.classed('hover',true); 332 | updateClasses(); 333 | 334 | dispatch.hover(d); 335 | } 336 | 337 | function updateClasses() { 338 | nodes 339 | .classed('hover', _hover) 340 | .classed('fixed', _fixed); 341 | 342 | links 343 | .classed('hover', _hover) 344 | .classed('fixed', _edgeFixed); 345 | } 346 | 347 | var _hoff = function(d) {d.hover = false; }; 348 | function mouseoutHighlight() { 349 | chart.container.classed('hover',false); 350 | 351 | nodes.each(_hoff); 352 | links.each(_hoff); 353 | 354 | updateClasses(); 355 | dispatch.hover(null); 356 | } 357 | 358 | // LINKS 359 | var gLinks = g.selectAll('g.links').data([graph._edges]); 360 | 361 | gLinks.enter() 362 | .append('g') 363 | .classed('links', true); 364 | 365 | links = gLinks.selectAll('.link') 366 | .data(_F(), linkName); 367 | 368 | links.enter().append('svg:path') 369 | .classed('link', true) 370 | .style({ 371 | fill: 'none', 372 | stroke: '#666', 373 | 'stroke-width': '1.5px', 374 | }) 375 | .on('mouseover.highlight',mouseoverEdgeHighlight) 376 | .on('mouseout.highlight',mouseoutHighlight) 377 | //.on('mouseover', tooltipShow) 378 | //.on('mouseout', tooltipHide) 379 | ; 380 | 381 | links 382 | .attr({ 383 | id: function(d) { return 'link-'+d._index; }, 384 | d: hiveLink, 385 | //'marker-end': function(d,i) { return d.type === 'pair' ? 'url(#arrow-'+i+')' : ''; } 386 | }) 387 | .style({ 388 | 'stroke-width': _slog, 389 | opacity: function(d) { return eopac(d.value); } 390 | }) 391 | ; 392 | 393 | links.exit().remove(); 394 | 395 | /* links.each(function(d, i) { 396 | if (d.type !== 'pair') {return;} 397 | 398 | var def = d3.select(markers[0][i]); 399 | var dy = def.attr('refX'); 400 | 401 | var l = this.getTotalLength(); 402 | var p0 = this.getPointAtLength(l); 403 | var pm1 = this.getPointAtLength(l-dy); 404 | 405 | var a = Math.atan((pm1.y-p0.y)/(pm1.x-p0.x)); 406 | 407 | def 408 | .attr('orient', degrees(a-Math.PI/2)); 409 | }); */ 410 | 411 | //console.log(graph.edges); 412 | 413 | // NODES 414 | var nodesLayer = g.selectAll('g.nodes').data([graph._nodes]); 415 | 416 | nodesLayer.enter() 417 | .append('g') 418 | .classed('nodes', true); 419 | 420 | // Select 421 | 422 | nodesLayer.selectAll('.node').remove(); // TODO: not this, hack to ensure nodes are stacked 423 | 424 | nodes = nodesLayer.selectAll('.node').data(_F(), _id); 425 | 426 | function nodeClick(d) { 427 | d3.event.stopPropagation(); 428 | 429 | var p = d.fixed; 430 | 431 | if (d3.event.altKey) { // remove 432 | d.ticked = (d.ticked) ? false : true; 433 | } else if (d3.event.ctrlKey && !d3.event.shiftKey) { // add to selection 434 | d.fixed = (d.fixed) ? false : true; 435 | } else if (d3.event.shiftKey) { // add all to selection 436 | graph.nodes.forEach(function(d) { 437 | d.fixed = (d.hover) ? !p : (!d3.event.ctrlKey) ? false : d.fixed; 438 | }); 439 | } else { // change selection 440 | graph.nodes.forEach(function(d) { 441 | d.fixed = false; 442 | }); 443 | d.fixed = (p) ? false : true; 444 | } 445 | 446 | updateClasses(); //function(d) { return d.source.fixed && d.target.fixed; }); 447 | dispatch.selectionChanged(d); 448 | } 449 | 450 | // Create 451 | var nodesEnter = nodes.enter().append('g') 452 | .classed('node', true) 453 | .style({ 454 | fill: '#ccc', 455 | 'fill-opacity': 1, 456 | stroke: '#333', 457 | 'stroke-width': '1px' 458 | }) 459 | .on('click', nodeClick) 460 | .on('dblclick', function(d) { 461 | //if (d3.event.defaultPrevented) {return;} 462 | 463 | //console.log(d3.event); 464 | d3.event.stopPropagation(); 465 | 466 | var outEdges = graph.outEdgesIndex[d.id]; 467 | 468 | for (var i = 0; i < outEdges.length; i++) { 469 | if (!outEdges[i].target.ticked) { 470 | //console.log(outEdges[i].target); 471 | outEdges[i].target.ticked = true; 472 | break; 473 | } 474 | } 475 | 476 | updateClasses(); 477 | dispatch.selectionChanged(d); 478 | 479 | //graph.outEdgesIndex[d.id].forEach(function(edge) { 480 | 481 | //}); 482 | 483 | /* if (d3.event.altKey || d3.event.ctrlKey) { 484 | d3.event.stopPropagation(); 485 | if (d3.event.altKey) { 486 | d.ticked = (d.ticked) ? false : true; 487 | } else if (d3.event.ctrlKey) { 488 | d.fixed = (d.fixed) ? false : true; 489 | } 490 | 491 | updateClasses(); //function(d) { return d.source.fixed && d.target.fixed; }); 492 | dispatch.selectionChanged(d); 493 | } */ 494 | 495 | }) 496 | .on('contextmenu', function(d) { 497 | d3.event.preventDefault(); 498 | dispatch.contextmenu(d); 499 | }) 500 | .on('mouseover.highlight', mouseoverNodeHighlight) //function() { nodeClassed.call(this, 'hover', true); }) 501 | .on('mouseout.highlight', mouseoutHighlight) //function() { nodeClassed.call(this, 'hover', false); }) 502 | //.on('mouseover', tooltipShow) 503 | //.on('mouseout', tooltipHide) 504 | ; 505 | 506 | //nodesEnter.append('rect') 507 | //.on('dblclick', tooltip.toggle) 508 | //.on('mouseout', tooltip.hide) 509 | //; 510 | 511 | function _t(d) { 512 | if (d.group === 'gene.ligand') { return 'A'; } 513 | if (d.group === 'gene.receptor') { return 'V'; } 514 | return 'circle'; 515 | } 516 | 517 | function _r(d) { return (d.type === 'gene') ? 90 : rsize(d.value)*15; } 518 | //function __r(d) { return -_r(d); } 519 | //function _2r(d) { return 2*_r(d); } 520 | //function rx(d) { return (d.type === 'gene') ? 0 : _r(d); } 521 | 522 | //var sym = d3.svg.symbol().type('circle').size(_r); 523 | 524 | var sym = hiveSymbol().type(_t).size(_r); 525 | 526 | nodesEnter.append('path') 527 | .attr('d', sym); 528 | 529 | nodesEnter.append('text') 530 | .style({ 531 | stroke: 'none', 532 | fill: '#333', 533 | 'stroke-width': '1px', 534 | 'font-size': '10px' 535 | }) 536 | .attr({ 537 | 'text-anchor': 'start', 538 | 'dy': 3, 539 | 'dx': 15 540 | }) 541 | .attr('transform', function(d) { 542 | return 'rotate( '+degrees(_labelAngle(d))+' )'; 543 | }); 544 | 545 | nodes 546 | .attr('id', function(d) { return 'node-'+d._index; }) 547 | .classed('fixed', _fixed) 548 | .attr('transform', function(d) { 549 | return 'rotate( '+degrees(_angle(d))+' ) translate(' + _radius(d) + ')'; 550 | //return 'rotate( '+degrees(_angle(d))+' ) translate(' + _radius(d) + ') rotate( '+degrees(_labelAngle(d))+' )'; 551 | }); 552 | 553 | nodes.each(function(d) { // Store the x-y position for output 554 | var b = this.getBoundingClientRect(); 555 | d.x = b.left; 556 | d.y = b.top; 557 | }); 558 | 559 | nodes.select('path') 560 | /* .attr({ 561 | x: __r, 562 | y: __r, 563 | width: _2r, 564 | height: _2r, 565 | rx: rx, 566 | ry: rx, 567 | }) */ 568 | .style('fill',_ncolor); 569 | 570 | nodes.select('text') 571 | .text(nodeName) 572 | .attr({ 573 | dy: function(d) { return (d.type === 'gene') ? 0 : 3; }, 574 | dx: function(d) { return (d.type === 'gene') ? 10 : 15; } 575 | }); 576 | 577 | nodes.exit().remove(); 578 | 579 | function legendHighlight(d) { 580 | 581 | if (d === null) { 582 | container.classed('hover', false); 583 | nodes.style('opacity', 1).classed('hover', false); 584 | return; 585 | } 586 | 587 | var h = _group.eq(d.group).and(_class.eq(d.class)); 588 | //function o(n) { return h(n) ? 1 : 0.2; }; 589 | 590 | nodes 591 | //.style('opacity', o) 592 | .classed('hover', h); 593 | 594 | container.classed('hover', true); 595 | 596 | } 597 | 598 | var legend = root.models.legend() 599 | .colors(_ncolor) 600 | .on('mouseover', _F(null, legendHighlight)) 601 | .on('mouseout', function() { legendHighlight(null); }); 602 | 603 | var _l = [ 604 | { name: 'Ligand expressing cell', class: 'ligand', group: 'sample' }, 605 | { name: 'Receptor expressing cell', class: 'receptor', group: 'sample' }, 606 | { name: 'Ligand and receptor expressing cell', class: 'both', group: 'sample' }, 607 | { name: 'Ligand gene', class: 'ligand', group: 'gene.ligand' }, 608 | { name: 'Receptor gene', class: 'receptor', group: 'gene.receptor' } 609 | ]; 610 | 611 | container.selectAll('.caxis').remove(); // TODO: not this 612 | 613 | container.append('g') 614 | .attr({ 615 | class: 'axis caxis', 616 | transform: 'translate('+(margin.left)+','+margin.top+')' 617 | }) 618 | .datum(_l) 619 | .call(legend); 620 | 621 | }; 622 | 623 | d3.rebind(chart, dispatch, 'on'); 624 | return chart; 625 | }; 626 | 627 | root.charts.hiveGraph = hiveGraph; 628 | root.models.hiveSymbol = hiveSymbol; 629 | 630 | })(window.lrd3); 631 | -------------------------------------------------------------------------------- /app/components/charts/force-chart.js: -------------------------------------------------------------------------------- 1 | /* global _F */ 2 | /* global d3 */ 3 | 4 | (function(root) { 5 | 'use strict'; 6 | 7 | var forceGraph = function() { 8 | var width = 500, height = 500; 9 | var margin = { top: 10, right: 60, bottom: 240, left: 60}; 10 | //var padding = { top: 60, right: 100, bottom: 60, left: 60}; 11 | 12 | function chart(selection) { 13 | selection.each(chart.draw); 14 | } 15 | 16 | // elements 17 | var nodes, links; 18 | 19 | // Private objects 20 | var zoom = d3.behavior.zoom(); 21 | 22 | var groups = ['gene.ligand','gene.receptor','sample']; 23 | 24 | // Scales 25 | var ncolor = d3.scale.ordinal() 26 | .domain(['ligand','both','receptor']) 27 | .range(['#ed1940','yellow','#3349ff']); 28 | //.range(['#ed1940','#a650e2','#9999ff']); 29 | //.range(['#ed1940','#a650e2','#3349ff']); 30 | var slog = d3.scale.log().range([2,9]).clamp(true); // Maps value to normalized edge width 31 | var eopac = d3.scale.linear().range([0.2,0.8]).clamp(true); 32 | var rsize = d3.scale.linear().range([3, 12]).clamp(true); // Maps value to size 33 | //var angle = d3.scale.ordinal().domain(groups).range([-Math.PI/4+Math.PI, Math.PI/4+Math.PI, 0]); // maps type to angle 34 | var radius = d3.scale.linear(); // maps position to radius 35 | 36 | var _y = {}; // maps index to position 37 | groups.forEach(function(d) { 38 | _y[d] = d3.scale.ordinal().rangePoints([0, 1]); 39 | }); 40 | 41 | // Value accessors 42 | //var _type = _F('type'); 43 | var _group = _F('group'); 44 | var _value = _F('value'); 45 | //var _name = _F('name'); 46 | var _id = _F('id'); 47 | var _class = _F('class'); 48 | var _fixed = _F('fixed'); 49 | //var _edgeFixed = _F('source',_fixed).and(_F('target',_fixed)); 50 | var _hover = _F('hover'); 51 | 52 | var linkName = function(d) { return [d.source.name, d.name, d.target.name].join(':'); }; 53 | var nodeName = function(d) { // TODO: not this 54 | return d.name.split('.')[0]; 55 | }; 56 | 57 | // Range accesors 58 | //var _angle = _F('group', angle); //function(d) { return angle(d.type); }; 59 | //var _radius = function(d) { return radius(_y[_group(d)](d._i)); }; 60 | var _ncolor = _F('class', ncolor); //function(d) { return ncolor(d.class); }; 61 | var _slog = _F(_value, slog); 62 | 63 | var dispatch = d3.dispatch('hover','selectionChanged'); 64 | 65 | chart.draw = function draw(graph) { 66 | 67 | var container = d3.select(this); 68 | 69 | width = parseInt(container.style('width')); 70 | height = parseInt(container.style('height')); 71 | 72 | var size = Math.min(height, width)/(1+Math.cos(Math.PI/3))/1.5; 73 | radius.range([size/10, size]); 74 | 75 | chart.update = function() { 76 | updateClasses(); 77 | }; 78 | 79 | chart.container = container; 80 | 81 | container 82 | .attr('width', width) 83 | .attr('height', height) 84 | ; 85 | 86 | container.on('click', function() { 87 | if (d3.event.defaultPrevented) {return;} 88 | d3.event.stopPropagation(); 89 | 90 | graph.nodes.forEach(function(d) { 91 | d.fixed = false; 92 | }); 93 | 94 | updateClasses(); 95 | dispatch.selectionChanged(); 96 | }); 97 | 98 | // Ranges 99 | var _e = d3.extent(graph._edges, _value); // Edge values 100 | slog.domain(_e); 101 | eopac.domain(_e); 102 | 103 | var _n = d3.extent(graph._nodes, _value); // Node values 104 | rsize.domain(_n); 105 | 106 | /* var nodesByType = d3.nest() // Nest nodes by type 107 | .key(_group) 108 | .sortKeys(d3.ascending) 109 | .entries(graph._nodes); 110 | 111 | nodesByType.forEach(function(type) { // Setup domain for position range 112 | _y[type.key].domain(d3.range(type.values.length)); 113 | 114 | type.values.forEach(function(node,i) { 115 | node._i = i; 116 | //node.group = group; 117 | }); 118 | }); */ 119 | 120 | function tick() { 121 | 122 | 123 | links.attr('d', function(d) { 124 | 125 | var 126 | x1 = d.source.x, 127 | y1 = d.source.y, 128 | x2 = d.target.x, 129 | y2 = d.target.y, 130 | dx = x2 - x1, 131 | dy = y2 - y1; 132 | 133 | var dr = Math.sqrt(dx * dx + dy * dy), 134 | drx = dr, 135 | dry = dr, 136 | xRotation = 0, // degrees 137 | largeArc = 0, // 1 or 0 138 | sweep = 1; // 1 or 0 139 | 140 | // Self edge. 141 | if ( x1 === x1 && y1 === y2 ) { 142 | 143 | // Needs to be 1. 144 | largeArc = 1; 145 | 146 | // Change sweep to change orientation of loop. 147 | sweep = 0; 148 | 149 | var dcx = x1 - width/2; 150 | var dcy = y1 - height/2; 151 | var dcr = Math.sqrt(dcx * dcx + dcy * dcy); 152 | 153 | xRotation = Math.atan(dcy/dcx || 0)*180/Math.PI; 154 | 155 | drx = 30; 156 | dry = 20; 157 | 158 | x2 += dcy/dcr; 159 | y2 -= dcx/dcr; 160 | 161 | } else { 162 | var _radius = rsize(d.target.value)+_slog(d)+5; 163 | var theta = Math.PI/6; //Math.atan(dy / dx || 0); 164 | 165 | var c = Math.cos(theta); 166 | var s = Math.sin(theta); 167 | 168 | x2 -= _radius * (dx/dr*c-dy/dr*s); 169 | y2 -= _radius * (dx/dr*s+dy/dr*c); 170 | //dx = x2-x1; 171 | //dy = y2-y1; 172 | 173 | //dr = drx = dry = Math.sqrt(dx * dx + dy * dy); 174 | } 175 | 176 | return 'M' + x1 + ',' + y1 + 'A' + drx + ',' + dry + ' ' + xRotation + ',' + largeArc + ',' + sweep + ' ' + x2 + ',' + y2; 177 | }); 178 | 179 | nodes 180 | .attr('transform', function(d) { 181 | return 'translate(' + d.x + ',' + d.y + ')'; 182 | }); 183 | 184 | } 185 | 186 | var force = d3.layout.force() // todo: move 187 | .charge(-2000) 188 | .linkDistance(function linkDistance(d) { 189 | return d.type === 'pair' ? 1 : 400; 190 | }) 191 | .size([width, height]) 192 | .on('tick', tick); 193 | 194 | graph._nodes.forEach(function(d) { 195 | delete d.x; 196 | delete d.y; 197 | }); 198 | 199 | force 200 | .nodes(graph._nodes) 201 | .links(graph._edges) 202 | .start(); 203 | 204 | var drag = force.drag() 205 | .on('dragstart', function() { 206 | d3.event.sourceEvent.stopPropagation(); 207 | }); 208 | 209 | // MARKERS 210 | container.selectAll('defs').remove(); 211 | 212 | container 213 | .append('defs') 214 | .append('svg:marker') 215 | .attr('class', 'Triangle') 216 | .attr('viewBox', '0 -5 10 10') 217 | .attr('refY', 0) 218 | .attr('refX', 2.5) 219 | .attr('markerWidth', 2.5) 220 | .attr('markerHeight', 2.5) 221 | .attr('stroke-width', 1) 222 | .attr('markerUnits','strokeWidth') 223 | //.style('stroke', function(d) { return color(d.value); }) 224 | //.style('fill', function(d) { return color(d.value); }) 225 | .attr('orient', 'auto') 226 | .attr('id', 'arrow') 227 | .append('svg:path') 228 | .attr('d', 'M0,-5L10,0L0,5'); 229 | 230 | var g = container.selectAll('.networkGraph').data([1]); 231 | 232 | g.enter() 233 | .append('g') 234 | .attr('class', 'networkGraph') 235 | .each(function() { // Only called once 236 | container.call(zoom.on('zoom', rescale)); 237 | zoom.translate([0,margin.top]); 238 | zoom.event(container); 239 | }); 240 | 241 | // rescale g 242 | function rescale() { 243 | var trans=d3.event.translate; 244 | var scale=d3.event.scale; 245 | 246 | g.attr('transform', 247 | 'translate(' + trans + ') scale(' + scale + ')'); 248 | } 249 | 250 | function classNeighbors(node, direction, key, value) { 251 | if (arguments.length < 4) { value = true; } 252 | 253 | var _tgt = (direction <= 0) ? 'source' : 'target'; 254 | //var _dir = (direction <= 0) ? 'lin' : 'lout'; 255 | 256 | var edges = (direction <= 0) ? graph._inEdgesIndex[node.id] : graph._outEdgesIndex[node.id]; 257 | 258 | edges.forEach(function(d) { 259 | if(d) { 260 | d[key] = value; // class edge 261 | 262 | var t = d[_tgt]; 263 | t[key] = value; // class node 264 | 265 | if (t.type === 'gene') { 266 | classNeighbors(t, direction, key, value); 267 | } 268 | 269 | } 270 | }); 271 | } 272 | 273 | function mouseoverEdgeHighlight(d) { 274 | 275 | var tgt = d.target; 276 | var src = d.source; 277 | 278 | d.hover = tgt.hover = src.hover = true; 279 | 280 | if (tgt.type !== 'node') { 281 | classNeighbors(tgt, 3, 'hover'); 282 | } 283 | 284 | if (src.type !== 'node') { 285 | classNeighbors(src, -3, 'hover'); 286 | } 287 | 288 | chart.container.classed('hover',true); 289 | updateClasses(); 290 | 291 | dispatch.hover(d); 292 | } 293 | 294 | function mouseoverNodeHighlight(d) { 295 | 296 | //var node = d3.select(this); 297 | 298 | d.hover = true; 299 | 300 | classNeighbors(d, 3, 'hover'); 301 | classNeighbors(d, -3, 'hover'); 302 | 303 | chart.container.classed('hover',true); 304 | updateClasses(); 305 | 306 | dispatch.hover(d); 307 | } 308 | 309 | function updateClasses() { 310 | //console.log('updateClasses'); 311 | 312 | nodes 313 | .classed('hover', _hover) 314 | .classed('fixed', _fixed); 315 | 316 | links 317 | .classed('hover', _hover) 318 | .classed('fixed', _fixed) 319 | //.attr('marker-end', function(d,i) { return (d.source !== d.target) ? 'url("#arrow-'+i+'")' : ''; }) 320 | ; 321 | } 322 | 323 | var _hoff = function(d) {d.hover = false; }; 324 | function mouseoutHighlight() { 325 | chart.container.classed('hover',false); 326 | 327 | nodes.each(_hoff); 328 | links.each(_hoff); 329 | 330 | updateClasses(); 331 | dispatch.hover(null); 332 | } 333 | 334 | function edgeClick(d) { 335 | if (d3.event.defaultPrevented) {return;} 336 | d3.event.stopPropagation(); 337 | 338 | var p = d.fixed; 339 | 340 | if (d3.event.altKey) { // remove 341 | d.ticked = p ? false : true; 342 | } else if (d3.event.ctrlKey && !d3.event.shiftKey) { // add to selection 343 | d.fixed = p ? false : true; 344 | } else if (d3.event.shiftKey) { // add all to selection 345 | graph.nodes.forEach(function(d) { 346 | d.fixed = (d.hover) ? !p : (!d3.event.ctrlKey) ? false : d.fixed; 347 | }); 348 | } else { // change selection 349 | graph.nodes.forEach(function(d) { 350 | d.fixed = false; 351 | }); 352 | d.fixed = (p) ? false : true; 353 | } 354 | 355 | updateClasses(); //function(d) { return d.source.fixed && d.target.fixed; }); 356 | dispatch.selectionChanged(d); 357 | } 358 | 359 | // LINKS 360 | var gLinks = g.selectAll('g.links').data([graph._edges]); 361 | 362 | gLinks.enter() 363 | .append('g') 364 | .classed('links', true); 365 | 366 | links = gLinks.selectAll('.link') 367 | .data(_F(), linkName); 368 | 369 | links.enter().append('path') 370 | .classed('link', true) 371 | .style({ 372 | fill: 'none', 373 | stroke: '#666', 374 | 'stroke-width': '1.5px', 375 | }) 376 | .style('opacity', function(d) { return eopac(d.value); }) 377 | .on('mouseover.highlight',mouseoverEdgeHighlight) 378 | .on('mouseout.highlight',mouseoutHighlight) 379 | .on('click',edgeClick) 380 | //.on('mouseover', tooltipShow) 381 | //.on('mouseout', tooltipHide) 382 | .attr('marker-end', function(d) { return (d.source !== d.target) ? 'url("#arrow")' : ''; }) 383 | ; 384 | 385 | links 386 | //.attr('id', function(d,i) { return 'link-'+i; }) 387 | .style('stroke-width', _slog) 388 | //.attr('d', hiveLink) 389 | //.attr('marker-end', function(d,i) { return (d.source !== d.target) ? 'url(#arrow-'+i+')' : 'none'; }) 390 | ; 391 | 392 | links.exit().remove(); 393 | 394 | // NODES 395 | var nodesLayer = g.selectAll('g.nodes').data([graph._nodes]); 396 | 397 | nodesLayer.enter() 398 | .append('g') 399 | .classed('nodes', true); 400 | 401 | // Select 402 | //nodesLayer.selectAll('.node').remove(); // TODO: not this, hack to ensure nodes are stacked 403 | 404 | nodes = nodesLayer.selectAll('.node').data(_F(), _id); 405 | 406 | function nodeClick(d) { 407 | if (d3.event.defaultPrevented) {return;} 408 | d3.event.stopPropagation(); 409 | 410 | var p = d.fixed; 411 | 412 | if (d3.event.altKey) { // remove 413 | d.ticked = (d.ticked) ? false : true; 414 | } else if (d3.event.ctrlKey && !d3.event.shiftKey) { // add to selection 415 | d.fixed = (d.fixed) ? false : true; 416 | } else if (d3.event.shiftKey) { // add all to selection 417 | graph.nodes.forEach(function(d) { 418 | d.fixed = (d.hover) ? !p : (!d3.event.ctrlKey) ? false : d.fixed; 419 | }); 420 | } else { // change selection 421 | graph.nodes.forEach(function(d) { 422 | d.fixed = false; 423 | }); 424 | d.fixed = (p) ? false : true; 425 | } 426 | 427 | updateClasses(); //function(d) { return d.source.fixed && d.target.fixed; }); 428 | dispatch.selectionChanged(d); 429 | } 430 | 431 | // Create 432 | var nodesEnter = nodes.enter().append('g') 433 | .classed('node', true) 434 | .style({fill: '#ccc','fill-opacity': 1,stroke: '#333','stroke-width': '1px'}) 435 | .on('click', nodeClick) 436 | .on('dblclick', function(d) { 437 | //if (d3.event.defaultPrevented) {return;} 438 | 439 | //console.log(d3.event); 440 | d3.event.stopPropagation(); 441 | 442 | var edges = d3.event.shiftKey ? graph.inEdgesIndex[d.id] : graph.outEdgesIndex[d.id]; 443 | //var alt = d3.event.altKey; 444 | 445 | if (d3.event.altKey) { 446 | for (var i = edges.length; i > -1; i--) { 447 | if (edges[i].type === 'sample-sample') { 448 | var node = d3.event.shiftKey ? edges[i].source : edges[i].target; 449 | if (node.ticked) { 450 | node.ticked = false; 451 | break; 452 | } 453 | } 454 | } 455 | } else { 456 | for (var j = 0; j < edges.length; j++) { 457 | if (edges[j].type === 'sample-sample') { 458 | var _node = d3.event.shiftKey ? edges[j].source : edges[j].target; 459 | if (!_node.ticked) { 460 | _node.ticked = true; 461 | break; 462 | } 463 | } 464 | } 465 | } 466 | 467 | updateClasses(); 468 | dispatch.selectionChanged(d); 469 | 470 | }) 471 | .on('mouseover.highlight', mouseoverNodeHighlight) //function() { nodeClassed.call(this, 'hover', true); }) 472 | .on('mouseout.highlight', mouseoutHighlight) //function() { nodeClassed.call(this, 'hover', false); }) 473 | //.on('mouseover', tooltipShow) 474 | //.on('mouseout', tooltipHide) 475 | .call(drag) 476 | ; 477 | 478 | nodesEnter 479 | .append('rect') 480 | //.on('dblclick', tooltip.toggle) 481 | //.on('mouseout', tooltip.hide) 482 | ; 483 | 484 | nodesEnter 485 | .append('text') 486 | .style({'stroke': 'none','fill': '#333','stroke-width': '1px','font-size': '10px'}) 487 | .attr('text-anchor', 'start') 488 | .attr('dy', 3) 489 | .attr('dx', 15) 490 | //.on('mouseover', nodeLabelTooltip.show) 491 | //.on('mouseout', nodeLabelTooltip.hide) 492 | ; 493 | 494 | nodes 495 | .attr('id', function(d) { return 'node-'+d._index; }) 496 | .classed('fixed', _fixed); 497 | 498 | function _r(d) { return (d.type === 'gene') ? 5 : rsize(d.value); } 499 | function __r(d) { return -_r(d); } 500 | function _2r(d) { return 2*_r(d); } 501 | function rx(d) { return (d.type === 'gene') ? 0 : _r(d); } 502 | 503 | nodes 504 | .select('rect') 505 | .attr('x',__r) 506 | .attr('y',__r) 507 | .attr('width',_2r) 508 | .attr('height',_2r) 509 | .attr('rx',rx) 510 | .attr('ry',rx) 511 | .style('fill',_ncolor); 512 | 513 | nodes 514 | .select('text') 515 | .text(nodeName) 516 | .attr('dy',function(d) { return (d.type === 'gene') ? 0 : 3; }) 517 | .attr('dx',function(d) { return (d.type === 'gene') ? 10 : 15; }) 518 | ; 519 | 520 | nodes.exit().remove(); 521 | 522 | function legendHighlight(d) { 523 | 524 | if (d === null) { 525 | container.classed('hover', false); 526 | nodes.classed('hover', false); 527 | return; 528 | } 529 | 530 | var h = _group.eq(d.group).and(_class.eq(d.class)); 531 | 532 | nodes 533 | .classed('hover', h); 534 | 535 | container.classed('hover', true); 536 | 537 | } 538 | 539 | var _l = [ 540 | { name: 'Ligand expressing cell', class: 'ligand', group: 'sample' }, 541 | { name: 'Receptor expressing cell', class: 'receptor', group: 'sample' }, 542 | { name: 'Ligand and receptor expressing cell', class: 'both', group: 'sample' }//, 543 | //{ name: 'Ligand gene', class: 'ligand', group: 'gene.ligand' }, 544 | //{ name: 'Receptor gene', class: 'receptor', group: 'gene.receptor' } 545 | ]; 546 | 547 | var legend = root.models.legend() 548 | .colors(_ncolor) 549 | .on('mouseover', _F(null, legendHighlight)) 550 | .on('mouseout', function() { legendHighlight(null); }); 551 | 552 | container.selectAll('.caxis').remove(); 553 | 554 | container.append('g') 555 | .attr('class', 'axis caxis') 556 | .attr('transform', 'translate('+(margin.left)+','+margin.top+')') 557 | .datum(_l) 558 | .call(legend); 559 | 560 | /* container.selectAll('.caxis').remove(); // TODO: not this 561 | 562 | var labels = container.selectAll('.caxis').data([_l]).enter().append('g') 563 | .attr('class', 'axis caxis') 564 | .attr('transform', 'translate('+(margin.left)+','+margin.top+')') 565 | .selectAll('.label').data(_F()).enter().append('g') 566 | .attr('class', 'label') 567 | .on('mouseover', _F(null, highlight)) //function(d) { highlight(d); }) //function(d) { highlight(d); } 568 | .on('mouseout', function() { highlight(null); }) 569 | .attr('transform', function(d,i) { return 'translate(0,'+((i+1)*20)+')'; }); 570 | 571 | var _rx = function rx(d) { return (d.name.match(/gene/)) ? 0 : 15; }; 572 | 573 | labels.append('rect') 574 | .style({'stroke-width': '1px', 'stroke': 'rgb(51, 51, 51)'}) 575 | .attr('width', 15) 576 | .attr('height', 15) 577 | .attr('rx',_rx) 578 | .attr('ry',_rx) 579 | .style('fill', _ncolor); 580 | 581 | labels.append('text') 582 | .style({'stroke': 'none','fill': '#333','stroke-width': '1px','font-size': '10px'}) 583 | .attr('x', 20) 584 | .attr('dy', '1.2em') 585 | .text(_F('name')); */ 586 | 587 | }; 588 | 589 | d3.rebind(chart, dispatch, 'on'); 590 | return chart; 591 | }; 592 | 593 | root.charts.forceGraph = forceGraph; 594 | 595 | })(window.lrd3); 596 | --------------------------------------------------------------------------------