diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 0000000..966fb07 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "bower" +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5760be5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a1994cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +._* +.~lock.* +.buildpath +.DS_Store +.idea +.project +.settings + +# Ignore node stuff +node_modules/ +npm-debug.log +libpeerconnection.log + +# OS-specific +.DS_Store + +# Bower components +bower diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..433ed80 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,62 @@ +{ + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "es3": false, + "forin": true, + "freeze": true, + "immed": true, + "indent": 2, + "latedef": "nofunc", + "newcap": true, + "noarg": true, + "noempty": true, + "nonbsp": true, + "nonew": true, + "plusplus": false, + "quotmark": "single", + "undef": true, + "unused": false, + "strict": false, + "maxparams": 10, + "maxdepth": 5, + "maxstatements": 40, + "maxcomplexity": 8, + "maxlen": 1200, + + "asi": false, + "boss": false, + "debug": false, + "eqnull": true, + "esnext": false, + "evil": false, + "expr": false, + "funcscope": false, + "globalstrict": false, + "iterator": false, + "lastsemic": false, + "laxbreak": false, + "laxcomma": false, + "loopfunc": true, + "maxerr": false, + "moz": false, + "multistr": false, + "notypeof": false, + "proto": false, + "scripturl": false, + "shadow": false, + "sub": true, + "supernew": false, + "validthis": false, + "noyield": false, + + "browser": true, + "node": true, + + "globals": { + "angular": false, + "$": false, + "d3": false + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..31285dc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: +- '4' +before_install: +- npm install -g gulp gulp-cli bower +- bower install +script: npm run ci diff --git a/.yo-rc.json b/.yo-rc.json new file mode 100644 index 0000000..a673f4f --- /dev/null +++ b/.yo-rc.json @@ -0,0 +1,35 @@ +{ + "generator-angular-package": { + "props": { + "moduleName": "angular-bubbletree", + "description": "Angular bubble tree chart using d3", + "gitUrl": "http://amgadfahmi.git", + "keywords": "[\"d3\",\"angular\",\"chart\",\"responsive\",\"related\",\"bubble\"]", + "license": "MIT", + "author": { + "name": "Amgad Fahmi", + "email": "amgadnady@gmail.com" + }, + "yourModule": { + "original": "angular-bubbletree", + "camelized": "angularBubbletree", + "dasherized": "angular-bubbletree", + "slugified": "angular-bubbletree", + "parts": [ + "angular", + "bubbletree" + ] + }, + "includeModuleDirectives": true, + "includeModuleFilters": true, + "includeModuleServices": true, + "includeModuleControllers": true, + "includeAngularModuleResource": false, + "includeAngularModuleCookies": false, + "includeAngularModuleSanitize": false, + "moduleDirectory": "modules/angular-bubbletree", + "moduleUnitTestDirectory": "test/unit/angular-bubbletree", + "moduleUnitE2eDirectory": "test/e2e/angular-bubbletree" + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index e69de29..e0b83b9 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,97 @@ +# angular-bubbletree +Master 0.1.0: [![Master Status](https://travis-ci.org/amgadfahmi/angular-bubbletree.svg?branch=master)](https://travis-ci.org/amgadfahmi/js-binarysearch)  Development 0.1.0: [![Development Status](https://travis-ci.org/amgadfahmi/angular-bubbletree.svg?branch=development)](https://travis-ci.org/amgadfahmi/js-binarysearch) [![Dependency Status](https://david-dm.org/amgadfahmi/angular-bubbletree.svg)](https://david-dm.org/amgadfahmi/angular-bubbletree) + +![angular d3 chart bubble tree](https://amgadfahmi.files.wordpress.com/2016/06/angular-bubbletree-chart.png "angular d3 chart bubble tree") + + + +## Installation + +[![bower](https://amgadfahmi.files.wordpress.com/2016/05/bower.png "Javascript Binary Search")](http://bower.io/search/?q=angular-bubbletree) + +You can install the package using Bower +``` +$ bower install --save js-binarysearch +``` +Or download the project and find the module in `dist/` folder + +## Usage +The directive is simple, define the directive in your html as following + +```html + //optional + +``` +And in your controller define the following +```javascript +//this very important to communicate with the directive +//and be able to call internal functionalities + $scope.chart = {}; + //load you data in anyway you want + $http.get('data.json').then(function(result) { + //once data is ready pass it to the bind function + $scope.chart.bind( result.data); + }); +``` + +## Parameters + +``` +parameter condition default description +id required NA defines the control id in the dom +control required NA refernece to scope object in order to communicate with the directive +width optional 1000 determine the svg container width +height optional 400 determine the svg container height +link-distance optional 100 determine the distanaation between each node +fill-style optional gradient changes the chart bubbles layout (solid - gradient) +parent-color optional #336699 color of any parent node (works only with solid style) +child-color optional #e0e0eb color of any child node (works only with solid style) +collapsed-color optional #19334d color of any collapsed parent node (works only with solid style) +link-color optional #9ecae1 color of link between nodes +enable-tooltip optional true show small tooltip beside the node(very basic) +text-x optional 1.45em determine the x coordinates of the node text according to its node +text-y optional 3.45em determine the y coordinates of the node text according to its node +scale-score optional 10 determine the scaling factor for node sizes(1 too small - 30 is big) +``` + +## Extra info + +![angular d3 chart bubble tree](https://amgadfahmi.files.wordpress.com/2016/06/angular-bubbletree-chart2.png "angular d3 chart bubble tree") + +You can override the default following classes in order to change the look of component +```css +.btNode circle { +} +.btNode text { +} +.btLink { +} +div.btTooltip{ +} +``` + +## License + +Copyright [2016] Amgad Fahmi + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..2d06799 --- /dev/null +++ b/bower.json @@ -0,0 +1,45 @@ +{ + "name": "angular-bubbletree", + "version": "0.1.0", + "description": "Angular bubble tree chart using d3", + "keywords": [ + "d3", + "angular", + "chart", + "responsive", + "related", + "bubble" + ], + "authors": [ + { + "name": "Amgad Fahmi", + "email": "amgadnady@gmail.com" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/amgadfahmi/angular-bubbletree.git" + }, + "license": "ALv2", + "main": [ + "dist/angular-bubbletree.js", + "dist/angular-bubbletree.css" + ], + "ignore": [ + "*.*", + "bower", + "modules", + "node_modules", + "src", + "test" + ], + "dependencies": { + "d3": "^3.5.17" + }, + "devDependencies": { + "angular-mocks": ">=1.2.0", + "angular-scenario": ">=1.2.0", + "angular": ">=1.2.0", + "d3": "^3.5.17" + } +} diff --git a/data.json b/data.json new file mode 100644 index 0000000..b34eefd --- /dev/null +++ b/data.json @@ -0,0 +1,65 @@ +{ + "name": "flare", + "children": [{ + "name": "analytics", + "children": [{ + "name": "cluster", + "children": [{ + "name": "Agglomerative Cluster", + "size": 3938 + }, { + "name": "Community Structure", + "size": 3812 + }, { + "name": "Hierarchical Cluster", + "size": 6714 + }, { + "name": "Merge Edge", + "size": 700 + }] + }, { + "name": "graph", + "children": [{ + "name": "Betweenness Centrality", + "size": 3534 + }, { + "name": "Link Distance", + "size": 5731 + }, { + "name": "Max Flow MinCut", + "size": 7840 + }, { + "name": "Shortest Paths", + "size": 5914 + }, { + "name": "Spanning Tree", + "size": 3416 + }] + }, { + "name": "optimization", + "children": [{ + "name": "Aspect Ratio Banker", + "size": 7074, + "children": [{ + "name": "Betweenness Centrality", + "size": 3534 + }, { + "name": "Link Distance", + "size": 5731 + }, { + "name": "Max Flow MinCut", + "size": 7840, + "children": [{ + "name": "Betweenness Centrality", + "size": 534 + }, { + "name": "Link Distance", + "size": 1731 + } + ] + } + ] + }] + }] + }] +} \ No newline at end of file diff --git a/dist/angular-bubbletree.css b/dist/angular-bubbletree.css new file mode 100644 index 0000000..b95713e --- /dev/null +++ b/dist/angular-bubbletree.css @@ -0,0 +1,43 @@ +.svg-container { + /*border: 4px solid red;*/ + width: 99%; + height: 600px; + background: #fff; + margin: 0 auto; +} + +.btNode circle { + cursor: pointer; + stroke: #3182bd; + stroke-width: 1.5px; +} + +.btNode text { + font: 10px sans-serif; + pointer-events: none; + text-anchor: middle; +} + +.btLink { + fill: none; + stroke: #9ecae1; + stroke-width: 1.5px; +} + +div.btTooltip { + position: absolute; + text-align: center; + vertical-align: middle; + width: 100px; + height: 38px; + padding: 2px; + font: 11px sans-serif; + color: #153851; + background: lightsteelblue; + border-radius: 8px; + pointer-events: none; + border-style: solid; + border-color: #8cb3d9; + border-width: 0.01em; + box-shadow: 2px 2px 5px #888888; +} \ No newline at end of file diff --git a/dist/angular-bubbletree.js b/dist/angular-bubbletree.js new file mode 100644 index 0000000..3a3af84 --- /dev/null +++ b/dist/angular-bubbletree.js @@ -0,0 +1,327 @@ +(function (angular) { + + // Create all modules and define dependencies to make sure they exist + // and are loaded in the correct order to satisfy dependency injection + // before all nested files are concatenated by Gulp + + // Config + angular.module('angular-bubbletree.config', []) + .value('angular-bubbletree.config', { + debug: true + }); + + // Modules + + angular.module('angular-bubbletree.directives', []); + + + angular.module('angular-bubbletree.filters', []); + + + angular.module('angular-bubbletree.services', []); + + + angular.module('angular-bubbletree.controllers', []); + + angular.module('angular-bubbletree', + [ + 'angular-bubbletree.config', + 'angular-bubbletree.directives', + 'angular-bubbletree.filters', + 'angular-bubbletree.services', + 'angular-bubbletree.controllers' + ]); + +})(angular); + +(function() { + 'use strict'; + angular.module('angular-bubbletree').directive('bubbletree', bubbletree); + bubbletree.$inject = ['$log', 'bubbletreeSvc']; + + function bubbletree(log, svc) { + + return { + name: 'bubbletree', + scope: { + datasource: '=', + control: '=' + }, + restrict: 'E', + replace: true, + transclude: false, + link: function postLink(scope, element, attrs) { + var width = '100%', + height = '100%', + force, svg, div, link, node, + datasource, scaledData = []; + + scope.internalCtrl = scope.control || {}; + + scope.internalCtrl.bind = function(data) { + log.debug('Binding bubble tree chart'); + if (data) { + datasource = data; + render(); + log.debug('Binding completed'); + } + }; + + function initialize() { + var dummy; + scope.params = {}; + dummy = attrs.id ? scope.params.id = attrs.id : scope.params.id = 'bubbletree'; + dummy = attrs.width ? scope.params.width = attrs.width : scope.params.width = 1000; + dummy = attrs.height ? scope.params.height = attrs.height : scope.params.height = 400; + dummy = attrs.linkDistance ? scope.params.linkDistance = attrs.linkDistance : scope.params.linkDistance = 100; + dummy = attrs.enableTooltip ? scope.params.enableTooltip = scope.$eval(attrs.enableTooltip) : scope.params.enableTooltip = true; + dummy = attrs.fillStyle && ['solid', 'gradient'].indexOf(attrs.fillStyle) >= 0 ? scope.params.fillStyle = attrs.fillStyle : scope.params.fillStyle = 'gradient'; + dummy = attrs.parentColor ? scope.params.parentColor = attrs.parentColor : scope.params.parentColor = '#336699'; + dummy = attrs.childColor ? scope.params.childColor = attrs.childColor : scope.params.childColor = '#e0e0eb'; + dummy = attrs.collapsedColor ? scope.params.collapsedColor = attrs.collapsedColor : scope.params.collapsedColor = '#19334d'; + dummy = attrs.linkColor ? scope.params.linkColor = attrs.linkColor : scope.params.linkColor = '#9ecae1'; + dummy = attrs.textX ? scope.params.textX = attrs.textX : scope.params.textX = '1.45em'; + dummy = attrs.textY ? scope.params.textY = attrs.textY : scope.params.textY = '3.45em'; + dummy = attrs.scaleScore ? scope.params.scaleScore = attrs.scaleScore : scope.params.scaleScore = 10; + + + force = d3.layout.force() + .linkDistance(scope.params.linkDistance) + .charge(-120) + .gravity(0.01) + .size([scope.params.width, scope.params.height]) + .on('tick', tick); + + svg = d3.select('#' + attrs.id) + .append('svg') + .attr('width', width) + .attr('height', height); + + link = svg.selectAll('.btLink'); + node = svg.selectAll('.btNode'); + + if (scope.params.enableTooltip) { + div = d3.select('#' + attrs.id).append('div') + .attr('class', 'btTooltip') + .style('opacity', 0); + } + + } + + initialize(); + + + function render() { + var nodes = svc.flatten(datasource, scaledData), + links = d3.layout.tree().links(nodes); + + // Restart the force layout. + force + .nodes(nodes) + .links(links) + .start(); + + // Update links. + link = link.data(links, function(d) { + return d.target.id; + }); + + link.exit().remove(); + + link.enter().insert('line', '.btNode') + .attr('class', 'btLink'); + if (scope.params.linkColor) { + link.attr('style', 'stroke:' + scope.params.linkColor + ';'); + } + + // Update nodes. + node = node.data(nodes, function(d) { + return d.id; + }); + + node.exit().remove(); + + var nodeEnter = node.enter().append('g') + .attr('class', 'btNode') + .on('click', click) + .call(force.drag); + + + nodeEnter.append('circle') + .on('mouseover', function(d) { + if (scope.params.enableTooltip) { + div.transition() + .duration(1000) + .style('opacity', 0.9); + + var pageX = d3.event.pageX; + var pageY = d3.event.pageY; + var data = d; + // http.get('exampleTooltip.html').then(function(result) { + // var element = angular.element('
'); + // element.append(result.data); + // var newScope = scope.$new(); + // newScope.node = data; + // newScope.test = 'i am data'; + // var compiled = compile(element)(newScope); + // console.log(compiled); + // // newScope.$digest(); + // div.html(compiled[0].innerHTML) + // .style('left', (pageX - 120) + 'px') + // .style('top', (pageY - 12) + 'px'); + // }); + var size = data.size ? data.size : ''; + div.html(data.name + '
' + size) + .style('left', (pageX - 120) + 'px') + .style('top', (pageY - 12) + 'px'); + } + + }) + .on('mouseout', function(d) { + if (scope.params.enableTooltip) { + div.transition() + .duration(500) + .style('opacity', 0); + } + }) + .attr('r', function(d) { + // console.log(svc.linearScale(d.size, datasource)); + var min = d3.min(scaledData); + var max = d3.max(scaledData); + return svc.linearScale(d.size, datasource, min, max, scope.params.scaleScore) || 10; + }); + + nodeEnter.append('text') + .attr('dy', scope.params.textY) + .attr('dx', scope.params.textX) + .text(function(d) { + return d.name; + }); + + node.select('circle') + .style('fill', function(d) { + if (scope.params.fillStyle === 'solid') { + return svc.colorSolid(d, scope.params); + } else { + return svc.colorGradient(svg, d, scope.params); + } + + }); + } + + function tick() { + link.attr('x1', function(d) { + return d.source.x; + }) + .attr('y1', function(d) { + return d.source.y; + }) + .attr('x2', function(d) { + return d.target.x; + }) + .attr('y2', function(d) { + return d.target.y; + }); + + node.attr('transform', function(d) { + return 'translate(' + d.x + ',' + d.y + ')'; + }); + } + + + // Toggle children on click. + function click(d) { + if (d3.event.defaultPrevented) { + return; + } // ignore drag + if (d.children) { + d._children = d.children; + d.children = null; + } else { + d.children = d._children; + d._children = null; + } + render(); + } + + } + + }; + } + +})(); +(function() { + 'use strict'; + angular.module('angular-bubbletree').service('bubbletreeSvc', bubbletreeSvc); + bubbletreeSvc.$inject = ['$log']; + + function bubbletreeSvc(log) { + var service = {}; + + service.linearScale = function(value, scaledData, min, max, scale) { + var scaled = d3.scale.linear() + .domain([min, max]) + .range([1 * scale, 2 * scale]); + return scaled(value); + }; + + // Returns a list of all nodes under the datasource. + service.flatten = function(datasource, scaledData) { + var nodes = [], + i = 0; + + function recurse(node) { + if (node.size) { + scaledData.push(node.size); + } + if (node.children) { + node.children.forEach(recurse); + } + if (!node.id) { + node.id = ++i; + } + nodes.push(node); + } + recurse(datasource); + return nodes; + }; + + service.colorSolid = function(d, params) { + return d._children ? params.collapsedColor //'#19334d' // collapsed package + : d.children ? params.parentColor //'#336699' // expanded package + : params.childColor; //'#e0e0eb'; // leaf node + }; + + service.colorGradient = function(svg, d) { + var color = '#fff', + id = 'btGradient'; + + // Define the gradient + var gradient = svg.append('svg:defs') + .append('svg:linearGradient') + .attr('id', id) + .attr('x1', '0%') + .attr('y1', '0%') + .attr('x2', '100%') + .attr('y2', '100%') + .attr('spreadMethod', 'pad'); + + // Define the gradient colors + gradient.append('svg:stop') + .attr('offset', '0%') + .attr('stop-color', '#999') + .attr('stop-opacity', 1); + + gradient.append('svg:stop') + .attr('offset', '100%') + .attr('stop-color', color) + .attr('stop-opacity', 1); + + return 'url(#' + id + ')'; + }; + + return service; + + } + +})(); \ No newline at end of file diff --git a/dist/angular-bubbletree.min.js b/dist/angular-bubbletree.min.js new file mode 100644 index 0000000..c1c841a --- /dev/null +++ b/dist/angular-bubbletree.min.js @@ -0,0 +1 @@ +!function(e){e.module("angular-bubbletree.config",[]).value("angular-bubbletree.config",{debug:!0}),e.module("angular-bubbletree.directives",[]),e.module("angular-bubbletree.filters",[]),e.module("angular-bubbletree.services",[]),e.module("angular-bubbletree.controllers",[]),e.module("angular-bubbletree",["angular-bubbletree.config","angular-bubbletree.directives","angular-bubbletree.filters","angular-bubbletree.services","angular-bubbletree.controllers"])}(angular),function(){"use strict";function e(e,t){return{name:"bubbletree",scope:{datasource:"=",control:"="},restrict:"E",replace:!0,transclude:!1,link:function(r,a,l){function n(){var e;r.params={},e=l.id?r.params.id=l.id:r.params.id="bubbletree",e=l.width?r.params.width=l.width:r.params.width=1e3,e=l.height?r.params.height=l.height:r.params.height=400,e=l.linkDistance?r.params.linkDistance=l.linkDistance:r.params.linkDistance=100,e=l.enableTooltip?r.params.enableTooltip=r.$eval(l.enableTooltip):r.params.enableTooltip=!0,e=l.fillStyle&&["solid","gradient"].indexOf(l.fillStyle)>=0?r.params.fillStyle=l.fillStyle:r.params.fillStyle="gradient",e=l.parentColor?r.params.parentColor=l.parentColor:r.params.parentColor="#336699",e=l.childColor?r.params.childColor=l.childColor:r.params.childColor="#e0e0eb",e=l.collapsedColor?r.params.collapsedColor=l.collapsedColor:r.params.collapsedColor="#19334d",e=l.linkColor?r.params.linkColor=l.linkColor:r.params.linkColor="#9ecae1",e=l.textX?r.params.textX=l.textX:r.params.textX="1.45em",e=l.textY?r.params.textY=l.textY:r.params.textY="3.45em",e=l.scaleScore?r.params.scaleScore=l.scaleScore:r.params.scaleScore=10,c=d3.layout.force().linkDistance(r.params.linkDistance).charge(-120).gravity(.01).size([r.params.width,r.params.height]).on("tick",i),u=d3.select("#"+l.id).append("svg").attr("width",f).attr("height",g),p=u.selectAll(".btLink"),b=u.selectAll(".btNode"),r.params.enableTooltip&&(d=d3.select("#"+l.id).append("div").attr("class","btTooltip").style("opacity",0))}function o(){var e=t.flatten(m,h),a=d3.layout.tree().links(e);c.nodes(e).links(a).start(),p=p.data(a,function(e){return e.target.id}),p.exit().remove(),p.enter().insert("line",".btNode").attr("class","btLink"),r.params.linkColor&&p.attr("style","stroke:"+r.params.linkColor+";"),b=b.data(e,function(e){return e.id}),b.exit().remove();var l=b.enter().append("g").attr("class","btNode").on("click",s).call(c.drag);l.append("circle").on("mouseover",function(e){if(r.params.enableTooltip){d.transition().duration(1e3).style("opacity",.9);var t=d3.event.pageX,a=d3.event.pageY,l=e,n=l.size?l.size:"";d.html(l.name+"
"+n).style("left",t-120+"px").style("top",a-12+"px")}}).on("mouseout",function(e){r.params.enableTooltip&&d.transition().duration(500).style("opacity",0)}).attr("r",function(e){var a=d3.min(h),l=d3.max(h);return t.linearScale(e.size,m,a,l,r.params.scaleScore)||10}),l.append("text").attr("dy",r.params.textY).attr("dx",r.params.textX).text(function(e){return e.name}),b.select("circle").style("fill",function(e){return"solid"===r.params.fillStyle?t.colorSolid(e,r.params):t.colorGradient(u,e,r.params)})}function i(){p.attr("x1",function(e){return e.source.x}).attr("y1",function(e){return e.source.y}).attr("x2",function(e){return e.target.x}).attr("y2",function(e){return e.target.y}),b.attr("transform",function(e){return"translate("+e.x+","+e.y+")"})}function s(e){d3.event.defaultPrevented||(e.children?(e._children=e.children,e.children=null):(e.children=e._children,e._children=null),o())}var c,u,d,p,b,m,f="100%",g="100%",h=[];r.internalCtrl=r.control||{},r.internalCtrl.bind=function(t){e.debug("Binding bubble tree chart"),t&&(m=t,o(),e.debug("Binding completed"))},n()}}}angular.module("angular-bubbletree").directive("bubbletree",e),e.$inject=["$log","bubbletreeSvc"]}(),function(){"use strict";function e(e){var t={};return t.linearScale=function(e,t,r,a,l){var n=d3.scale.linear().domain([r,a]).range([1*l,2*l]);return n(e)},t.flatten=function(e,t){function r(e){e.size&&t.push(e.size),e.children&&e.children.forEach(r),e.id||(e.id=++l),a.push(e)}var a=[],l=0;return r(e),a},t.colorSolid=function(e,t){return e._children?t.collapsedColor:e.children?t.parentColor:t.childColor},t.colorGradient=function(e,t){var r="#fff",a="btGradient",l=e.append("svg:defs").append("svg:linearGradient").attr("id",a).attr("x1","0%").attr("y1","0%").attr("x2","100%").attr("y2","100%").attr("spreadMethod","pad");return l.append("svg:stop").attr("offset","0%").attr("stop-color","#999").attr("stop-opacity",1),l.append("svg:stop").attr("offset","100%").attr("stop-color",r).attr("stop-opacity",1),"url(#"+a+")"},t}angular.module("angular-bubbletree").service("bubbletreeSvc",e),e.$inject=["$log"]}(); \ No newline at end of file diff --git a/example.html b/example.html new file mode 100644 index 0000000..2da9337 --- /dev/null +++ b/example.html @@ -0,0 +1,53 @@ + + + +Angular Bubble Tree + + + + + + + +
+ + +

Angular Bubble Tree

+ + +Click the following button to lead the data from data.json and bind the chart
+If you are facing an issues or have an enquiry, please submit a case on ithub issue here + + + + +
+ + diff --git a/exampleTooltip.html b/exampleTooltip.html new file mode 100644 index 0000000..76cba33 --- /dev/null +++ b/exampleTooltip.html @@ -0,0 +1,4 @@ +

Soon

+{{node.name}} +{{test}} +

end

\ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..9062920 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,151 @@ +var gulp = require('gulp'); +var Server = require('karma').Server; +var concat = require('gulp-concat'); +var uglify = require('gulp-uglify'); +var rename = require('gulp-rename'); +var path = require('path'); +var plumber = require('gulp-plumber'); +var runSequence = require('run-sequence'); +var jshint = require('gulp-jshint'); +var concatCss = require('gulp-concat-css'); +var templateCache = require('gulp-angular-templatecache'); +var eventStream = require('event-stream'); +var webserver = require('gulp-webserver'); + +/** + * File patterns + **/ + +// Root directory +var rootDirectory = path.resolve('./'); + +// Source directory for build process +var sourceDirectory = path.join(rootDirectory, './modules'); + +var sourceFiles = [ + + // Make sure module files are handled first + path.join(sourceDirectory, '/**/*.module.js'), + + // Then add all JavaScript files + path.join(sourceDirectory, '/**/*.js') +]; + +var stylesheets = [ + path.join(sourceDirectory, '/**/*.css') +]; + +var templates = [ + path.join(sourceDirectory, '/**/*.tpl.html') +]; + +var lintFiles = [ + 'gulpfile.js', + // Karma configuration + 'karma-*.conf.js' +].concat(sourceFiles); + +function getTemplateCache() { + return gulp.src(templates) + .pipe(plumber()) + .pipe(templateCache({ + module: 'angular-bubbletree.directives' + })); +} + +gulp.task('build', function() { + return eventStream.merge(gulp.src(sourceFiles), getTemplateCache()) + .pipe(plumber()) + .pipe(concat('angular-bubbletree.js')) + .pipe(gulp.dest('./dist/')) + .pipe(uglify()) + .pipe(rename('angular-bubbletree.min.js')) + .pipe(gulp.dest('./dist')); +}); + +gulp.task('build-stylesheets', function() { + return gulp.src(stylesheets) + .pipe(plumber()) + .pipe(concatCss('angular-bubbletree.css')) + .pipe(gulp.dest('./dist')); +}); + +/** + * Process + */ +gulp.task('process-all', function(done) { + runSequence('jshint', 'test-src', 'build', done); +}); + +/** + * Watch task + */ +gulp.task('watch', function() { + + // Watch JavaScript files + gulp.watch([sourceFiles, templates], ['process-all']); + gulp.watch([stylesheets], ['build-stylesheets']); +}); + +/** + * Validate source JavaScript + */ +gulp.task('jshint', function() { + gulp.src(lintFiles) + .pipe(plumber()) + .pipe(jshint()) + .pipe(jshint.reporter('jshint-stylish')) + .pipe(jshint.reporter('fail')); +}); + +/** + * Run test once and exit + */ +gulp.task('test-src', function(done) { + new Server({ + configFile: __dirname + '/karma-src.conf.js', + singleRun: true + }, done).start(); + +}); + +/** + * Run test once and exit + */ +gulp.task('test-dist-concatenated', function(done) { + new Server({ + configFile: __dirname + '/karma-dist-concatenated.conf.js', + singleRun: true + }, done).start(); +}); + +/** + * Run test once and exit + */ +gulp.task('test-dist-minified', function(done) { + new Server({ + configFile: __dirname + '/karma-dist-minified.conf.js', + singleRun: true + }, done).start(); + + // karma.start({ + // configFile: __dirname + '/karma-dist-minified.conf.js', + // singleRun: true + // }, done); +}); + +gulp.task('webserver', function() { + gulp.src('./') + .pipe(webserver({ + host: 'localhost', + port: 8000, + open: 'http://localhost:8000/example.html', + livereload: true, + directoryListing: true + // open: true + })); +}); + +gulp.task('default', function() { + runSequence('process-all', 'build-stylesheets', 'watch', 'webserver'); +}); \ No newline at end of file diff --git a/karma-dist-concatenated.conf.js b/karma-dist-concatenated.conf.js new file mode 100644 index 0000000..f35f207 --- /dev/null +++ b/karma-dist-concatenated.conf.js @@ -0,0 +1,77 @@ +// Karma configuration +// Generated on Thu Aug 21 2014 10:24:39 GMT+0200 (CEST) + +module.exports = function(config) { + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha', 'chai-jquery', 'jquery-1.8.3', 'sinon-chai'], + + plugins: [ + 'karma-mocha', + 'karma-chai', + 'karma-sinon-chai', + 'karma-chrome-launcher', + 'karma-phantomjs-launcher', + 'karma-jquery', + 'karma-chai-jquery' + ], + + // list of files / patterns to load in the browser + files: [ + 'bower/angular/angular.js', + 'bower/angular-mocks/angular-mocks.js', + 'dist/angular-bubbletree.js', + 'test/unit/**/*.js' + ], + + + // list of files to exclude + exclude: [ + ], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['PhantomJS'], + + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false + }); +}; diff --git a/karma-dist-minified.conf.js b/karma-dist-minified.conf.js new file mode 100644 index 0000000..62c5686 --- /dev/null +++ b/karma-dist-minified.conf.js @@ -0,0 +1,77 @@ +// Karma configuration +// Generated on Thu Aug 21 2014 10:24:39 GMT+0200 (CEST) + +module.exports = function(config) { + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha', 'chai-jquery', 'jquery-1.8.3', 'sinon-chai'], + + plugins: [ + 'karma-mocha', + 'karma-chai', + 'karma-sinon-chai', + 'karma-chrome-launcher', + 'karma-phantomjs-launcher', + 'karma-jquery', + 'karma-chai-jquery' + ], + + // list of files / patterns to load in the browser + files: [ + 'bower/angular/angular.js', + 'bower/angular-mocks/angular-mocks.js', + 'dist/angular-bubbletree.min.js', + 'test/unit/**/*.js' + ], + + + // list of files to exclude + exclude: [ + ], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['PhantomJS'], + + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false + }); +}; diff --git a/karma-src.conf.js b/karma-src.conf.js new file mode 100644 index 0000000..49fc31d --- /dev/null +++ b/karma-src.conf.js @@ -0,0 +1,79 @@ +// Karma configuration +// Generated on Thu Aug 21 2014 10:24:39 GMT+0200 (CEST) + +module.exports = function(config) { + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha', 'chai-jquery', 'jquery-1.8.3', 'sinon-chai'], + + plugins: [ + 'karma-mocha', + 'karma-chai', + 'karma-sinon-chai', + 'karma-chrome-launcher', + 'karma-phantomjs-launcher', + 'karma-jquery', + 'karma-chai-jquery', + 'karma-spec-reporter' + ], + + // list of files / patterns to load in the browser + files: [ + 'bower/angular/angular.js', + 'bower/angular-mocks/angular-mocks.js', + 'modules/**/*.module.js', + 'modules/**/*.js', + 'test/unit/**/*.js' + ], + + + // list of files to exclude + exclude: [ + ], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress', 'spec'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['PhantomJS'], + + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false + }); +}; diff --git a/modules/angular-bubbletree/angular-bubbletree.module.js b/modules/angular-bubbletree/angular-bubbletree.module.js new file mode 100644 index 0000000..7364c80 --- /dev/null +++ b/modules/angular-bubbletree/angular-bubbletree.module.js @@ -0,0 +1,35 @@ +(function (angular) { + + // Create all modules and define dependencies to make sure they exist + // and are loaded in the correct order to satisfy dependency injection + // before all nested files are concatenated by Gulp + + // Config + angular.module('angular-bubbletree.config', []) + .value('angular-bubbletree.config', { + debug: true + }); + + // Modules + + angular.module('angular-bubbletree.directives', []); + + + angular.module('angular-bubbletree.filters', []); + + + angular.module('angular-bubbletree.services', []); + + + angular.module('angular-bubbletree.controllers', []); + + angular.module('angular-bubbletree', + [ + 'angular-bubbletree.config', + 'angular-bubbletree.directives', + 'angular-bubbletree.filters', + 'angular-bubbletree.services', + 'angular-bubbletree.controllers' + ]); + +})(angular); diff --git a/modules/angular-bubbletree/directives/bubbletree.css b/modules/angular-bubbletree/directives/bubbletree.css new file mode 100644 index 0000000..b5293b5 --- /dev/null +++ b/modules/angular-bubbletree/directives/bubbletree.css @@ -0,0 +1,45 @@ +.svg-container { + /*border: 4px solid red;*/ + width: 99%; + height: 600px; + background: #fff; + margin: 0 auto; +} + + + +.btNode circle { + cursor: pointer; + stroke: #3182bd; + stroke-width: 1.5px; +} + +.btNode text { + font: 10px sans-serif; + pointer-events: none; + text-anchor: middle; +} + +.btLink { + fill: none; + stroke: #9ecae1; + stroke-width: 1.5px; +} + +div.btTooltip { + position: absolute; + text-align: center; + vertical-align: middle; + width: 100px; + height: 38px; + padding: 2px; + font: 11px sans-serif; + color: #153851; + background: lightsteelblue; + border-radius: 8px; + pointer-events: none; + border-style: solid; + border-color: #8cb3d9; + border-width: 0.01em; + box-shadow: 2px 2px 5px #888888; +} \ No newline at end of file diff --git a/modules/angular-bubbletree/directives/bubbletree.directive.js b/modules/angular-bubbletree/directives/bubbletree.directive.js new file mode 100644 index 0000000..9dab119 --- /dev/null +++ b/modules/angular-bubbletree/directives/bubbletree.directive.js @@ -0,0 +1,216 @@ +(function() { + 'use strict'; + angular.module('angular-bubbletree').directive('bubbletree', bubbletree); + bubbletree.$inject = ['$log', 'bubbletreeSvc']; + + function bubbletree(log, svc) { + + return { + name: 'bubbletree', + scope: { + datasource: '=', + control: '=' + }, + restrict: 'E', + replace: true, + transclude: false, + link: function postLink(scope, element, attrs) { + var width = '100%', + height = '100%', + force, svg, div, link, node, + datasource, scaledData = []; + + scope.internalCtrl = scope.control || {}; + + scope.internalCtrl.bind = function(data) { + log.debug('Binding bubble tree chart'); + if (data) { + datasource = data; + render(); + log.debug('Binding completed'); + } + }; + + function initialize() { + var dummy; + scope.params = {}; + dummy = attrs.id ? scope.params.id = attrs.id : scope.params.id = 'bubbletree'; + dummy = attrs.width ? scope.params.width = attrs.width : scope.params.width = 1000; + dummy = attrs.height ? scope.params.height = attrs.height : scope.params.height = 400; + dummy = attrs.linkDistance ? scope.params.linkDistance = attrs.linkDistance : scope.params.linkDistance = 100; + dummy = attrs.enableTooltip ? scope.params.enableTooltip = scope.$eval(attrs.enableTooltip) : scope.params.enableTooltip = true; + dummy = attrs.fillStyle && ['solid', 'gradient'].indexOf(attrs.fillStyle) >= 0 ? scope.params.fillStyle = attrs.fillStyle : scope.params.fillStyle = 'gradient'; + dummy = attrs.parentColor ? scope.params.parentColor = attrs.parentColor : scope.params.parentColor = '#336699'; + dummy = attrs.childColor ? scope.params.childColor = attrs.childColor : scope.params.childColor = '#e0e0eb'; + dummy = attrs.collapsedColor ? scope.params.collapsedColor = attrs.collapsedColor : scope.params.collapsedColor = '#19334d'; + dummy = attrs.linkColor ? scope.params.linkColor = attrs.linkColor : scope.params.linkColor = '#9ecae1'; + dummy = attrs.textX ? scope.params.textX = attrs.textX : scope.params.textX = '1.45em'; + dummy = attrs.textY ? scope.params.textY = attrs.textY : scope.params.textY = '3.45em'; + dummy = attrs.scaleScore ? scope.params.scaleScore = attrs.scaleScore : scope.params.scaleScore = 10; + + + force = d3.layout.force() + .linkDistance(scope.params.linkDistance) + .charge(-120) + .gravity(0.01) + .size([scope.params.width, scope.params.height]) + .on('tick', tick); + + svg = d3.select('#' + attrs.id) + .append('svg') + .attr('width', width) + .attr('height', height); + + link = svg.selectAll('.btLink'); + node = svg.selectAll('.btNode'); + + if (scope.params.enableTooltip) { + div = d3.select('#' + attrs.id).append('div') + .attr('class', 'btTooltip') + .style('opacity', 0); + } + + } + + initialize(); + + + function render() { + var nodes = svc.flatten(datasource, scaledData), + links = d3.layout.tree().links(nodes); + + // Restart the force layout. + force + .nodes(nodes) + .links(links) + .start(); + + // Update links. + link = link.data(links, function(d) { + return d.target.id; + }); + + link.exit().remove(); + + link.enter().insert('line', '.btNode') + .attr('class', 'btLink'); + if (scope.params.linkColor) { + link.attr('style', 'stroke:' + scope.params.linkColor + ';'); + } + + // Update nodes. + node = node.data(nodes, function(d) { + return d.id; + }); + + node.exit().remove(); + + var nodeEnter = node.enter().append('g') + .attr('class', 'btNode') + .on('click', click) + .call(force.drag); + + + nodeEnter.append('circle') + .on('mouseover', function(d) { + if (scope.params.enableTooltip) { + div.transition() + .duration(1000) + .style('opacity', 0.9); + + var pageX = d3.event.pageX; + var pageY = d3.event.pageY; + var data = d; + // http.get('exampleTooltip.html').then(function(result) { + // var element = angular.element('
'); + // element.append(result.data); + // var newScope = scope.$new(); + // newScope.node = data; + // newScope.test = 'i am data'; + // var compiled = compile(element)(newScope); + // console.log(compiled); + // // newScope.$digest(); + // div.html(compiled[0].innerHTML) + // .style('left', (pageX - 120) + 'px') + // .style('top', (pageY - 12) + 'px'); + // }); + var size = data.size ? data.size : ''; + div.html(data.name + '
' + size) + .style('left', (pageX - 120) + 'px') + .style('top', (pageY - 12) + 'px'); + } + + }) + .on('mouseout', function(d) { + if (scope.params.enableTooltip) { + div.transition() + .duration(500) + .style('opacity', 0); + } + }) + .attr('r', function(d) { + // console.log(svc.linearScale(d.size, datasource)); + var min = d3.min(scaledData); + var max = d3.max(scaledData); + return svc.linearScale(d.size, datasource, min, max, scope.params.scaleScore) || 10; + }); + + nodeEnter.append('text') + .attr('dy', scope.params.textY) + .attr('dx', scope.params.textX) + .text(function(d) { + return d.name; + }); + + node.select('circle') + .style('fill', function(d) { + if (scope.params.fillStyle === 'solid') { + return svc.colorSolid(d, scope.params); + } else { + return svc.colorGradient(svg, d, scope.params); + } + + }); + } + + function tick() { + link.attr('x1', function(d) { + return d.source.x; + }) + .attr('y1', function(d) { + return d.source.y; + }) + .attr('x2', function(d) { + return d.target.x; + }) + .attr('y2', function(d) { + return d.target.y; + }); + + node.attr('transform', function(d) { + return 'translate(' + d.x + ',' + d.y + ')'; + }); + } + + + // Toggle children on click. + function click(d) { + if (d3.event.defaultPrevented) { + return; + } // ignore drag + if (d.children) { + d._children = d.children; + d.children = null; + } else { + d.children = d._children; + d._children = null; + } + render(); + } + + } + + }; + } + +})(); \ No newline at end of file diff --git a/modules/angular-bubbletree/services/bubbletree.service.js b/modules/angular-bubbletree/services/bubbletree.service.js new file mode 100644 index 0000000..bee438e --- /dev/null +++ b/modules/angular-bubbletree/services/bubbletree.service.js @@ -0,0 +1,75 @@ +(function() { + 'use strict'; + angular.module('angular-bubbletree').service('bubbletreeSvc', bubbletreeSvc); + bubbletreeSvc.$inject = ['$log']; + + function bubbletreeSvc(log) { + var service = {}; + + service.linearScale = function(value, scaledData, min, max, scale) { + var scaled = d3.scale.linear() + .domain([min, max]) + .range([1 * scale, 2 * scale]); + return scaled(value); + }; + + // Returns a list of all nodes under the datasource. + service.flatten = function(datasource, scaledData) { + var nodes = [], + i = 0; + + function recurse(node) { + if (node.size) { + scaledData.push(node.size); + } + if (node.children) { + node.children.forEach(recurse); + } + if (!node.id) { + node.id = ++i; + } + nodes.push(node); + } + recurse(datasource); + return nodes; + }; + + service.colorSolid = function(d, params) { + return d._children ? params.collapsedColor //'#19334d' // collapsed package + : d.children ? params.parentColor //'#336699' // expanded package + : params.childColor; //'#e0e0eb'; // leaf node + }; + + service.colorGradient = function(svg, d) { + var color = '#fff', + id = 'btGradient'; + + // Define the gradient + var gradient = svg.append('svg:defs') + .append('svg:linearGradient') + .attr('id', id) + .attr('x1', '0%') + .attr('y1', '0%') + .attr('x2', '100%') + .attr('y2', '100%') + .attr('spreadMethod', 'pad'); + + // Define the gradient colors + gradient.append('svg:stop') + .attr('offset', '0%') + .attr('stop-color', '#999') + .attr('stop-opacity', 1); + + gradient.append('svg:stop') + .attr('offset', '100%') + .attr('stop-color', color) + .attr('stop-opacity', 1); + + return 'url(#' + id + ')'; + }; + + return service; + + } + +})(); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..5e4a215 --- /dev/null +++ b/package.json @@ -0,0 +1,65 @@ +{ + "name": "angular-bubbletree", + "version": "0.1.0", + "description": "Angular bubble tree chart using d3", + "keywords": [ + "d3", + "angular", + "chart", + "responsive", + "related", + "bubble" + ], + "author": { + "name": "Amgad Fahmi", + "email": "amgadnady@gmail.com" + }, + "scripts": { + "ci": "gulp test-src" + }, + "repository": { + "type": "git", + "url": "https://github.com/amgadfahmi/angular-bubbletree.git" + }, + "license": "ALv2", + "dependencies": { + "d3": "^3.5.17" + }, + "devDependencies": { + "chai": "^3.5.0", + "chai-jquery": "^2.0.0", + "event-stream": "^3.3.2", + "gulp": "^3.8.7", + "gulp-angular-templatecache": "^1.8.0", + "gulp-concat": "^2.3.4", + "gulp-concat-css": "^2.2.0", + "gulp-jshint": "^2.0.1", + "gulp-plumber": "^1.1.0", + "gulp-rename": "^1.2.0", + "gulp-scss": "^1.3.15", + "gulp-uglify": "^1.5.3", + "gulp-webserver": "^0.9.1", + "jshint": "^2.9.2", + "jshint-stylish": "^2.2.0", + "karma": "^0.13.22", + "karma-chai": "^0.1.0", + "karma-chai-jquery": "^1.0.0", + "karma-chrome-launcher": "^1.0.1", + "karma-jasmine": "^1.0.2", + "karma-jquery": "^0.1.0", + "karma-mocha": "^1.0.1", + "karma-ng-html2js-preprocessor": "^1.0.0", + "karma-phantomjs-launcher": "^1.0.0", + "karma-sinon-chai": "^1.2.0", + "karma-spec-reporter": "^0.0.26", + "mocha": "^2.5.3", + "ng-html2js": "^2.0.0", + "phantomjs-prebuilt": "^2.1.7", + "run-sequence": "^1.0.2", + "sinon": "^1.10.3", + "sinon-chai": "^2.5.0" + }, + "engines": { + "node": ">=0.8.0" + } +} diff --git a/test/unit/angular-bubbletree/angular-bubbletreeSpec.js b/test/unit/angular-bubbletree/angular-bubbletreeSpec.js new file mode 100644 index 0000000..e170161 --- /dev/null +++ b/test/unit/angular-bubbletree/angular-bubbletreeSpec.js @@ -0,0 +1,48 @@ +'use strict'; + +describe('initialize', function() { + + var module; + var dependencies; + dependencies = []; + + var hasModule = function(module) { + return dependencies.indexOf(module) >= 0; + }; + + beforeEach(function() { + + // Get module + module = angular.module('angular-bubbletree'); + dependencies = module.requires; + }); + + it('should load config module', function() { + expect(hasModule('angular-bubbletree.config')).to.be.ok; + }); + + + it('should load filters module', function() { + expect(hasModule('angular-bubbletree.filters')).to.be.ok; + }); + + + + it('should load directives module', function() { + expect(hasModule('angular-bubbletree.directives')).to.be.ok; + }); + + + + it('should load services module', function() { + expect(hasModule('angular-bubbletree.services')).to.be.ok; + }); + + + + it('should load controllers module', function() { + expect(hasModule('angular-bubbletree.controllers')).to.be.ok; + }); + + +});