├── .bowerrc ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── dev └── index.html ├── dist ├── angular-datamaps.js └── angular-datamaps.min.js ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── src ├── angular-datamaps.js └── directives │ └── datamaps-directive.js ├── test └── directives │ └── datamap.spec.js └── usaMap.png /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "json": "bower.json" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.js] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.html] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.less] 21 | indent_style = space 22 | indent_size = 2 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | -------------------------------------------------------------------------------- /.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": false, 20 | "smarttabs": true, 21 | "white": false, 22 | "globals": { 23 | "$": false, 24 | "angular": false, 25 | "browser": false, 26 | "repeater": false, 27 | "element": false, 28 | "inject": false, 29 | "afterEach": false, 30 | "beforeEach": false, 31 | "confirm": false, 32 | "context": false, 33 | "describe": false, 34 | "ddescribe": false, 35 | "expect": false, 36 | "it": false, 37 | "iit": false, 38 | "jasmine": false, 39 | "JSHINT": false, 40 | "mostRecentAjaxRequest": false, 41 | "qq": false, 42 | "runs": false, 43 | "spyOn": false, 44 | "spyOnEvent": false, 45 | "waitsFor": false, 46 | "xdescribe": false, 47 | "URI": false, 48 | "_": false, 49 | "input": false, 50 | "sleep": false, 51 | "pause": false, 52 | "protractor": false, 53 | "moment": false, 54 | "Datamap": false, 55 | "d3": false 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 2 | ###### _May 25, 2015_ 3 | 4 | ##### Breaking Changes 5 | - Refactored scope API variables 6 | - `scope.map` is now the only required input, and maps directly to the object required by Datamaps 7 | 8 | ##### General 9 | - Updated Datamaps dependency to v0.4.0 10 | - Remove unneccessary non-semantic markup hindering Datamap rendering 11 | - API for loading plugins for Datamaps has been added 12 | - Example plugin with a custom legend included in Readme 13 | - Responsive binding has been added 14 | - Zoomable option has been added 15 | - Allow updateChoropleth when geographies don't change 16 | - Slightly cleaner watch 17 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | 5 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 6 | 7 | grunt.initConfig({ 8 | 9 | config: { 10 | src: 'src/**/*.js', 11 | dist: 'dist', 12 | dev: 'dev', 13 | unit: 'test/**/*.js', 14 | e2e: 'e2e/**/*.js', 15 | module: 'angular-datamaps.js', 16 | minified: 'angular-datamaps.min.js' 17 | }, 18 | 19 | // Testing 20 | karma: { 21 | options: { 22 | configFile: 'karma.conf.js', 23 | files: [ 24 | 'bower_components/es5-shim/es5-shim.js', 25 | 'bower_components/angular/angular.js', 26 | 'bower_components/angular-mocks/angular-mocks.js', 27 | '<%= config.src %>', 28 | '<%= config.unit %>' 29 | ] 30 | }, 31 | unit: { 32 | options: { 33 | browsers: [ 34 | 'Chrome' 35 | ] 36 | }, 37 | background: true 38 | }, 39 | continuous: { 40 | options: { 41 | browsers: [ 42 | 'PhantomJS' 43 | ] 44 | }, 45 | singleRun: true 46 | } 47 | }, 48 | 49 | // Validation 50 | jshint: { 51 | options: { 52 | jshintrc: '.jshintrc' 53 | }, 54 | all: [ 55 | 'Gruntfile.js', 56 | '<%= config.src %>', 57 | '<%= config.unit %>', 58 | '<%= config.e2e %>' 59 | ] 60 | }, 61 | 62 | // Developing 63 | watch: { 64 | gruntfile: { 65 | files: ['Gruntfile.js'], 66 | tasks: ['build'] 67 | }, 68 | js: { 69 | files: ['<%= config.src %>', '<%= config.unit %>'], 70 | tasks: ['concat', 'karma:unit:run', 'jshint'] 71 | }, 72 | livereload: { 73 | options: { 74 | livereload: true 75 | }, 76 | files: ['<%= config.dist %>/<%= config.module %>'] 77 | } 78 | }, 79 | 80 | // The actual grunt server settings 81 | connect: { 82 | options: { 83 | port: 9000, 84 | livereload: 35729, 85 | // Change this to '0.0.0.0' to access the server from outside 86 | hostname: 'localhost' 87 | }, 88 | livereload: { 89 | options: { 90 | open: true, 91 | base: [ 92 | '.tmp', 93 | 'bower_components', 94 | '<%= config.dist %>', 95 | '<%= config.dev %>', 96 | ] 97 | } 98 | }, 99 | }, 100 | 101 | // Building 102 | clean: { 103 | dist: '<%= config.dist %>', 104 | dev: '.tmp' 105 | }, 106 | 107 | concat: { 108 | dev: { 109 | files: { 110 | '<%= config.dist %>/<%= config.module %>': '<%= config.src %>' 111 | } 112 | } 113 | }, 114 | 115 | ngmin: { 116 | dist: { 117 | files: { 118 | '<%= config.dist %>/<%= config.module %>': '<%= config.dist %>/<%= config.module %>' 119 | } 120 | } 121 | }, 122 | 123 | uglify: { 124 | dist: { 125 | files: { 126 | '<%= config.dist %>/<%= config.minified %>': '<%= config.dist %>/<%= config.module %>' 127 | }, 128 | options: { 129 | compress: {} 130 | } 131 | } 132 | }, 133 | 134 | wiredep: { 135 | target: { 136 | src: ['<%= config.dev %>/*.html'], 137 | } 138 | }, 139 | 140 | // Releasing 141 | bump: { 142 | options: { 143 | files: [ 144 | 'package.json', 145 | 'bower.json' 146 | ], 147 | updateConfigs: [], 148 | commit: true, 149 | commitMessage: '%VERSION%', 150 | commitFiles: [ 151 | 'CHANGELOG.md', 152 | 'package.json', 153 | 'bower.json', 154 | 'dist/angular-datamaps.js', 155 | 'dist/angular-datamaps.min.js' 156 | ], 157 | createTag: true, 158 | tagName: '%VERSION%', 159 | tagMessage: 'Version %VERSION%', 160 | push: true, 161 | pushTo: 'origin', 162 | gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d' // options to use with '$ git describe' 163 | } 164 | } 165 | }); 166 | 167 | grunt.registerTask('test', [ 168 | 'jshint', 169 | 'karma:continuous' 170 | ]); 171 | 172 | grunt.registerTask('dev', [ 173 | 'clean:dev', 174 | 'concat:dev', 175 | 'ngmin:dist', 176 | 'jshint', 177 | 'connect:livereload', 178 | 'watch' 179 | ]); 180 | 181 | grunt.registerTask('autotest', [ 182 | 'concat', 183 | 'karma:unit', 184 | 'karma:unit:run', 185 | 'watch' 186 | ]); 187 | 188 | grunt.registerTask('build', [ 189 | 'clean:dist', 190 | 'concat', 191 | 'ngmin:dist', 192 | 'uglify:dist', 193 | ]); 194 | 195 | grunt.registerTask('default', [ 196 | 'build', 197 | 'test' 198 | ]); 199 | 200 | grunt.registerTask('travis', [ 201 | 'jshint', 202 | 'test' 203 | ]); 204 | 205 | grunt.registerTask('release', [ 206 | 'build', 207 | 'bump' 208 | ]); 209 | 210 | }; 211 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Drew Machat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Datamaps 2 | 3 | ### Note: This directive's scope values have changed as of v0.1.0 to better match the object structure used by DataMaps. 4 | 5 | Provides an Angular directive to wrap https://github.com/markmarkoh/datamaps and easily build data maps in your Angular application. 6 | 7 | - Automatically updates on changes to bound data and options 8 | - onClick events integrate with your parent controllers 9 | - Evaluates plugins passed to the directive 10 | - Easily toggle zoom functionality 11 | - Documentation for available options can be found at https://github.com/markmarkoh/datamaps 12 | 13 | ![Datamap example](/usaMap.png?raw=true "USA Map Example") 14 | 15 | ## Install 16 | Install with npm and save to your project's package.json 17 | ```sh 18 | npm install angular-datamaps --save 19 | ``` 20 | 21 | Install with bower and save to your project's bower.json 22 | ```sh 23 | bower install angular-datamaps --save 24 | ``` 25 | 26 | Add the module to your app dependencies and include it in your page. 27 | ```js 28 | angular.module('app', [ 29 | 'datamaps' 30 | ]); 31 | ``` 32 | 33 | Load DataMaps and the two libraries DataMaps depends on (d3 and topojson). 34 | ```html 35 | 36 | 37 | 38 | 39 | 40 | 43 | 44 | ``` 45 | 46 | Add a map configuration object to your scope to bind to the directive 47 | ```js 48 | $scope.mapObject = { 49 | scope: 'usa', 50 | options: { 51 | width: 1110, 52 | legendHeight: 60 // optionally set the padding for the legend 53 | }, 54 | geographyConfig: { 55 | highlighBorderColor: '#EAA9A8', 56 | highlighBorderWidth: 2 57 | }, 58 | fills: { 59 | 'HIGH': '#CC4731', 60 | 'MEDIUM': '#306596', 61 | 'LOW': '#667FAF', 62 | 'defaultFill': '#DDDDDD' 63 | }, 64 | data: { 65 | "AZ": { 66 | "fillKey": "MEDIUM", 67 | }, 68 | "CO": { 69 | "fillKey": "HIGH", 70 | }, 71 | "DE": { 72 | "fillKey": "LOW", 73 | }, 74 | "GA": { 75 | "fillKey": "MEDIUM", 76 | } 77 | }, 78 | } 79 | ``` 80 | 81 | ### Geography click events ### 82 | The DataMaps click event can trigger a bound function with the clicked geography object. just add your custom function to the `on-click` attribute, like this (notice there are no parenthesis): 83 | 84 | ```html 85 | 89 | 90 | ``` 91 | 92 | Then in your controller, that function gets the selected geography object as it's argument, like so: 93 | 94 | ```js 95 | $scope.updateActiveGeography = function(geography) { 96 | $scope.stateName = geography.properties.name; 97 | $scope.stateCode = geography.id; 98 | } 99 | ``` 100 | 101 | ### Toggle zoom ### 102 | Set the `zoomable` attribute to toggle a simple zoom on the map. 103 | 104 | ### Responsive ### 105 | Bind the built-in Datamaps responsive methods by setting `$scope.mapObject.responsive = true`. 106 | 107 | ### Animated Update Choropleth ### 108 | Set `options.staticGeoData = true` to allow the map to update with only `updateChoropleth`. Update choropleth only works if _updating_ is all we're doing. If geographies are added or removed from data, we have to redraw the map, so use this to explicitly say whether or not the directive can update choropleth mappings only. 109 | 110 | ### Adding plugins ### 111 | You may add plugins that will be evaluated by the DataMaps plugin system in order to extend the labels or legend, for example. Use it by providing an object with plugin functions keyed by name. 112 | 113 | Data may be supplied to plugins through the `plugin-data`. This should be an object with keys corresponding to plugin names. 114 | 115 | If you would like to pass data into a core Datamaps plugin, be sure to include an empty entry for the plugin in the `plugin` object. This will ensure that gets processed. Datamaps won't override a plugin that is already defined. 116 | 117 | ```html 118 | 123 | 124 | ``` 125 | 126 | ```js 127 | $scope.mapObject = mapObject; 128 | $scope.mapPlugins = { 129 | bubbles: null, 130 | customLegend: function(layer, data, options) { 131 | var html = [''); 140 | d3.select(this.options.element).append('div') 141 | .attr('class', 'datamaps-legend') 142 | .html(html.join('')); 143 | } 144 | }; 145 | $scope.mapPluginData = { 146 | bubbles: [{name: 'Bubble 1', latitude: 21.32, longitude: -7.32, radius: 45, fillKey: 'gt500'}] 147 | }; 148 | 149 | ``` 150 | 151 | ## Build it yourself! 152 | angular-datamaps is built with grunt. 153 | 154 | To run a simple connect server to see the directive in action or to develop 155 | ```sh 156 | grunt dev 157 | ``` 158 | 159 | To run the tests 160 | ```sh 161 | grunt test 162 | ``` 163 | 164 | or run in autotest mode 165 | 166 | ```sh 167 | grunt autotest 168 | ``` 169 | 170 | And when you're done minify it 171 | ```sh 172 | grunt build 173 | ``` 174 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-datamaps", 3 | "description": "AngularJS Datamaps -- provides an Angular directive to wrap https://github.com/markmarkoh/datamaps", 4 | "version": "0.1.2", 5 | "author": "Drew Machat", 6 | "main": "dist/angular-datamaps.min.js", 7 | "dependencies": { 8 | "angular": ">=1.0.8", 9 | "datamaps": "~0.4.0" 10 | }, 11 | "devDependencies": { 12 | "es5-shim": "~2.1.0", 13 | "json3": "~3.2.5", 14 | "html5shiv": "~3.7.0", 15 | "respond": "~1.3.0", 16 | "angular-mocks": ">=1.0.8", 17 | "bootstrap": "~3.0.0" 18 | }, 19 | "ignore": [ 20 | ".DS_Store", 21 | ".git", 22 | ".bowerrc", 23 | ".editorconfig", 24 | ".gitignore", 25 | ".jshintrc", 26 | "bower.json", 27 | "package.json", 28 | "README.md", 29 | "Gruntfile.js", 30 | "usaMap.png", 31 | "karma.conf.js", 32 | "protractor.conf.js", 33 | "bower_components", 34 | "node_modules", 35 | "src", 36 | "dev", 37 | "test", 38 | "e2e" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular Datamaps Example 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 336 | 337 | 338 |
339 |
340 |
341 |

Angular Datamaps

342 |

An Angular directive to display maps with d3 and Datamaps

343 |
    344 |
  • Lightweight
  • 345 |
  • Automatically updates on changes to bound data and options
  • 346 |
  • No dependencies beside d3, topojson and https://github.com/markmarkoh/datamaps
  • 347 |
348 |
349 |
350 | 351 |
352 |
353 |

Simple Example - USA

354 |
355 |
356 | 361 | 362 |
363 |
364 |
365 |
366 | 367 | 368 | 369 | 370 |
371 |
372 |
373 |
374 | 375 | Map Width 376 |
377 |
378 |
379 |
380 | 381 |
382 |
383 |

World Map

384 |
385 |
386 | 389 | 390 |
391 |
392 | 393 |
394 | 395 | 396 | -------------------------------------------------------------------------------- /dist/angular-datamaps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('datamaps', []); 3 | 'use strict'; 4 | angular.module('datamaps').directive('datamap', [ 5 | '$window', 6 | function ($window) { 7 | return { 8 | restrict: 'EA', 9 | scope: { 10 | map: '=', 11 | plugins: '=?', 12 | zoomable: '@?', 13 | onClick: '&?', 14 | pluginData: '=' 15 | }, 16 | link: function (scope, element, attrs) { 17 | // Generate base map options 18 | function mapOptions() { 19 | return { 20 | element: element[0], 21 | scope: 'usa', 22 | height: scope.height, 23 | width: scope.width, 24 | fills: { defaultFill: '#b9b9b9' }, 25 | data: {}, 26 | done: function (datamap) { 27 | function redraw() { 28 | datamap.svg.selectAll('g').attr('transform', 'translate(' + d3.event.translate + ')scale(' + d3.event.scale + ')'); 29 | } 30 | if (angular.isDefined(attrs.onClick)) { 31 | datamap.svg.selectAll('.datamaps-subunit').on('click', function (geography) { 32 | scope.onClick()(geography); 33 | }); 34 | } 35 | if (angular.isDefined(attrs.zoomable)) { 36 | datamap.svg.call(d3.behavior.zoom().on('zoom', redraw)); 37 | } 38 | } 39 | }; 40 | } 41 | scope.api = { 42 | refresh: function (map) { 43 | scope.api.updateWithOptions(map); 44 | }, 45 | updateWithOptions: function (map) { 46 | // Clearing 47 | scope.api.clearElement(); 48 | // Update bounding box 49 | scope.width = (map.options || {}).width || null; 50 | scope.height = (map.options || {}).height || (scope.width ? scope.width * 0.5 : null); 51 | scope.legendHeight = (map.options || {}).legendHeight || 50; 52 | // Set a few defaults for the directive 53 | scope.mapOptions = mapOptions(); 54 | // Add the good stuff 55 | scope.mapOptions = angular.extend(scope.mapOptions, map); 56 | scope.datamap = new Datamap(scope.mapOptions); 57 | // Add responsive listeners 58 | if (scope.mapOptions.responsive) { 59 | $window.addEventListener('resize', scope.api.resize); 60 | } else { 61 | $window.removeEventListener('resize', scope.api.resize); 62 | } 63 | // Update plugins 64 | scope.api.updatePlugins(scope.datamap); 65 | // Update options and choropleth 66 | scope.api.refreshOptions(map.options); 67 | scope.api.updateWithData(map.data); 68 | }, 69 | updatePlugins: function (datamap) { 70 | if (!scope.plugins) { 71 | return; 72 | } 73 | var pluginData = scope.pluginData || {}; 74 | angular.forEach(scope.plugins, function (plugin, name) { 75 | datamap.addPlugin(name, plugin); 76 | datamap[name](pluginData[name]); 77 | }); 78 | }, 79 | refreshOptions: function (options) { 80 | if (!options) { 81 | return; 82 | } 83 | // set labels 84 | if (options.labels) { 85 | scope.datamap.labels({ 86 | labelColor: options.labelColor ? options.labelColor : '#333333', 87 | fontSize: options.labelSize ? options.labelSize : 12 88 | }); 89 | } 90 | // set legend 91 | if (options.legend) { 92 | scope.datamap.legend(); 93 | } 94 | }, 95 | resize: function () { 96 | scope.datamap.resize(); 97 | }, 98 | updateWithData: function (data) { 99 | scope.datamap.updateChoropleth(data); 100 | scope.api.updatePlugins(scope.datamap); 101 | }, 102 | clearElement: function () { 103 | scope.datamap = null; 104 | element.empty().css({ 105 | 'position': 'relative', 106 | 'display': 'block', 107 | 'padding-bottom': scope.legendHeight + 'px' 108 | }); 109 | } 110 | }; 111 | // Watch data changing 112 | scope.$watch('map', function (map, old) { 113 | // Return if no data 114 | if (!map || angular.equals({}, map)) { 115 | return; 116 | } 117 | // Allow animated transition when geos don't change 118 | // or fully refresh 119 | if (!scope.datamap || angular.equals(map.data, old.data)) { 120 | scope.api.refresh(map); 121 | } else if ((map.options || {}).staticGeoData) { 122 | scope.api.updateWithData(map.data); 123 | } else { 124 | scope.api.refresh(map); 125 | } 126 | }, true); 127 | //update the plugins if the pluginData has changed 128 | scope.$watch('pluginData', function () { 129 | scope.api.updatePlugins(scope.datamap); 130 | }, true); 131 | } 132 | }; 133 | } 134 | ]); -------------------------------------------------------------------------------- /dist/angular-datamaps.min.js: -------------------------------------------------------------------------------- 1 | "use strict";angular.module("datamaps",[]),angular.module("datamaps").directive("datamap",["$window",function(a){return{restrict:"EA",scope:{map:"=",plugins:"=?",zoomable:"@?",onClick:"&?",pluginData:"="},link:function(b,c,d){function e(){return{element:c[0],scope:"usa",height:b.height,width:b.width,fills:{defaultFill:"#b9b9b9"},data:{},done:function(a){function c(){a.svg.selectAll("g").attr("transform","translate("+d3.event.translate+")scale("+d3.event.scale+")")}angular.isDefined(d.onClick)&&a.svg.selectAll(".datamaps-subunit").on("click",function(a){b.onClick()(a)}),angular.isDefined(d.zoomable)&&a.svg.call(d3.behavior.zoom().on("zoom",c))}}}b.api={refresh:function(a){b.api.updateWithOptions(a)},updateWithOptions:function(c){b.api.clearElement(),b.width=(c.options||{}).width||null,b.height=(c.options||{}).height||(b.width?.5*b.width:null),b.legendHeight=(c.options||{}).legendHeight||50,b.mapOptions=e(),b.mapOptions=angular.extend(b.mapOptions,c),b.datamap=new Datamap(b.mapOptions),b.mapOptions.responsive?a.addEventListener("resize",b.api.resize):a.removeEventListener("resize",b.api.resize),b.api.updatePlugins(b.datamap),b.api.refreshOptions(c.options),b.api.updateWithData(c.data)},updatePlugins:function(a){if(b.plugins){var c=b.pluginData||{};angular.forEach(b.plugins,function(b,d){a.addPlugin(d,b),a[d](c[d])})}},refreshOptions:function(a){a&&(a.labels&&b.datamap.labels({labelColor:a.labelColor?a.labelColor:"#333333",fontSize:a.labelSize?a.labelSize:12}),a.legend&&b.datamap.legend())},resize:function(){b.datamap.resize()},updateWithData:function(a){b.datamap.updateChoropleth(a),b.api.updatePlugins(b.datamap)},clearElement:function(){b.datamap=null,c.empty().css({position:"relative",display:"block","padding-bottom":b.legendHeight+"px"})}},b.$watch("map",function(a,c){a&&!angular.equals({},a)&&(!b.datamap||angular.equals(a.data,c.data)?b.api.refresh(a):(a.options||{}).staticGeoData?b.api.updateWithData(a.data):b.api.refresh(a))},!0),b.$watch("pluginData",function(){b.api.updatePlugins(b.datamap)},!0)}}}]); -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | 3 | config.set({ 4 | 5 | // base path, that will be used to resolve files and exclude 6 | basePath: '', 7 | 8 | frameworks: ['jasmine'], 9 | 10 | plugins: [ 11 | 'karma-growl-reporter', 12 | 'karma-jasmine', 13 | 'karma-chrome-launcher', 14 | 'karma-phantomjs-launcher', 15 | 'karma-ie-launcher' 16 | ], 17 | 18 | /** 19 | * How to report, by default. 20 | */ 21 | reporters: ['dots', 'growl'], 22 | 23 | /** 24 | * On which port should the browser connect, on which port is the test runner 25 | * operating, and what is the URL path for the browser to use. 26 | */ 27 | //hostname: 'leon-work.local', 28 | port: 9018, 29 | runnerPort: 9100, 30 | urlRoot: '/', 31 | 32 | /** 33 | * Disable file watching by default. 34 | */ 35 | autoWatch: false 36 | }); 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-datamaps", 3 | "version": "0.1.2", 4 | "description": "AngularJS Datamaps -- provides an Angular directive to wrap https://github.com/markmarkoh/datamaps", 5 | "main": "dist/angular-datamaps.min.js", 6 | "scripts": { 7 | "test": "grunt travis --verbose" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com/dmachat/angular-datamaps.git" 12 | }, 13 | "keywords": [ 14 | "angular", 15 | "d3", 16 | "maps", 17 | "data", 18 | "viz", 19 | "directive" 20 | ], 21 | "author": "Drew Machat ", 22 | "bugs": { 23 | "url": "https://github.com/dmachat/angular-datamaps/issues" 24 | }, 25 | "devDependencies": { 26 | "matchdep": "~0.3.0", 27 | "grunt": "~0.4.1", 28 | "grunt-contrib-clean": "~0.5.0", 29 | "grunt-contrib-watch": "~0.5.1", 30 | "grunt-contrib-jshint": "~0.8.0", 31 | "grunt-contrib-concat": "~0.3.0", 32 | "grunt-contrib-uglify": "~0.3.1", 33 | "grunt-contrib-sass": "~0.8.1", 34 | "grunt-contrib-connect": "~0.8.0", 35 | "grunt-wiredep": "~1.8.0", 36 | "grunt-ngmin": "~0.0.3", 37 | "grunt-bump": "0.0.13", 38 | "karma": "~0.10.5", 39 | "grunt-karma": "~0.6.2", 40 | "karma-phantomjs-launcher": "~0.1.0", 41 | "karma-chrome-launcher": "~0.1.0", 42 | "karma-ie-launcher": "~0.1.1", 43 | "karma-growl-reporter": "~0.1.1", 44 | "protractor": "~0.16.1", 45 | "ejs": "~0.8.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | 3 | name: 'angular-upload', 4 | seleniumAddress: 'http://localhost:4444/wd/hub', 5 | //sauceUser: "leon", 6 | //sauceKey: "1234", 7 | specs: ['e2e/**/*.js'], 8 | 9 | capabilities: { 10 | browserName: 'chrome' 11 | }, 12 | 13 | baseUrl: 'http://localhost:9001', 14 | 15 | rootElement: 'body', 16 | 17 | jasmineNodeOpts: { 18 | onComplete: null, 19 | isVerbose: true, 20 | showColors: true, 21 | includeStackTrace: true, 22 | defaultTimeoutInterval: 30000 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/angular-datamaps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('datamaps', []); 4 | -------------------------------------------------------------------------------- /src/directives/datamaps-directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular 4 | 5 | .module('datamaps') 6 | 7 | .directive('datamap', ['$window', function($window) { 8 | return { 9 | restrict: 'EA', 10 | scope: { 11 | map: '=', //datamaps objects [required] 12 | plugins: '=?', //datamaps plugins [optional] 13 | zoomable: '@?', //zoomable toggle [optional] 14 | onClick: '&?', //geography onClick event [optional], 15 | pluginData: '=' //datamaps plugin data object where keys are plugin names [optional] 16 | }, 17 | link: function(scope, element, attrs) { 18 | 19 | // Generate base map options 20 | function mapOptions() { 21 | return { 22 | element: element[0], 23 | scope: 'usa', 24 | height: scope.height, 25 | width: scope.width, 26 | aspectRatio: scope.aspectRatio, 27 | fills: { 28 | defaultFill: '#b9b9b9' 29 | }, 30 | data: {}, 31 | done: function(datamap) { 32 | function redraw() { 33 | datamap.svg.selectAll('g') 34 | .attr('transform', 'translate(' + d3.event.translate + ')scale(' + d3.event.scale + ')'); 35 | } 36 | if (angular.isDefined(attrs.onClick)) { 37 | datamap.svg.selectAll('.datamaps-subunit').on('click', function(geography) { 38 | scope.onClick()(geography); 39 | }); 40 | } 41 | if (angular.isDefined(attrs.zoomable)) { 42 | datamap.svg.call(d3.behavior.zoom() 43 | .on('zoom', redraw)); 44 | } 45 | } 46 | }; 47 | } 48 | 49 | scope.api = { 50 | 51 | // Fully refresh directive 52 | refresh: function(map) { 53 | scope.api.updateWithOptions(map); 54 | }, 55 | 56 | // Update chart with new options 57 | updateWithOptions: function(map) { 58 | 59 | // Clearing 60 | scope.api.clearElement(); 61 | 62 | // Update bounding box 63 | scope.width = (map.options || {}).width || null; 64 | scope.height = (map.options || {}).height || (scope.width ? scope.width * 0.5 : null); 65 | scope.aspectRatio = (map.options || {}).aspectRatio || null; 66 | scope.legendHeight = (map.options || {}).legendHeight || 50; 67 | 68 | // Set a few defaults for the directive 69 | scope.mapOptions = mapOptions(); 70 | 71 | // Add the good stuff 72 | scope.mapOptions = angular.extend(scope.mapOptions, map); 73 | 74 | scope.datamap = new Datamap(scope.mapOptions); 75 | 76 | // Add responsive listeners 77 | if (scope.mapOptions.responsive) { 78 | $window.addEventListener('resize', scope.api.resize); 79 | } else { 80 | $window.removeEventListener('resize', scope.api.resize); 81 | } 82 | 83 | // Update plugins 84 | scope.api.updatePlugins(scope.datamap); 85 | 86 | // Update options and choropleth 87 | scope.api.refreshOptions(map.options); 88 | scope.api.updateWithData(map.data); 89 | }, 90 | 91 | // Add and initialize optional plugins 92 | updatePlugins: function(datamap) { 93 | if (!scope.plugins) { 94 | return; 95 | } 96 | 97 | var pluginData = scope.pluginData || {}; 98 | 99 | angular.forEach(scope.plugins, function(plugin, name) { 100 | datamap.addPlugin(name, plugin); 101 | datamap[name](pluginData[name]); 102 | }); 103 | }, 104 | 105 | // Set options on the datamap 106 | refreshOptions: function(options) { 107 | if (!options) { 108 | return; 109 | } 110 | 111 | // set labels 112 | if (options.labels) { 113 | scope.datamap.labels({ 114 | labelColor: options.labelColor ? options.labelColor : '#333333', 115 | fontSize: options.labelSize ? options.labelSize : 12 116 | }); 117 | } 118 | 119 | // set legend 120 | if (options.legend) { 121 | scope.datamap.legend(); 122 | } 123 | }, 124 | 125 | // Trigger datamaps resize method 126 | resize: function() { 127 | scope.datamap.resize(); 128 | }, 129 | 130 | // Update chart with new data 131 | updateWithData: function(data) { 132 | scope.datamap.updateChoropleth(data); 133 | scope.api.updatePlugins(scope.datamap); 134 | }, 135 | 136 | // Fully clear directive element 137 | clearElement: function () { 138 | scope.datamap = null; 139 | element 140 | .empty() 141 | .css({ 142 | 'position': 'relative', 143 | 'display': 'block', 144 | 'padding-bottom': scope.legendHeight + 'px' 145 | }); 146 | } 147 | }; 148 | 149 | // Watch data changing 150 | scope.$watch('map', function(map, old) { 151 | // Return if no data 152 | if (!map || angular.equals({}, map)) { 153 | return; 154 | } 155 | // Allow animated transition when geos don't change 156 | // or fully refresh 157 | if (!scope.datamap || angular.equals(map.data, old.data)) { 158 | scope.api.refresh(map); 159 | } else if ((map.options || {}).staticGeoData) { 160 | scope.api.updateWithData(map.data); 161 | } else { 162 | scope.api.refresh(map); 163 | } 164 | }, true); 165 | 166 | //update the plugins if the pluginData has changed 167 | scope.$watch('pluginData', function(){ 168 | scope.api.updatePlugins(scope.datamap); 169 | }, true); 170 | } 171 | }; 172 | }]); 173 | -------------------------------------------------------------------------------- /test/directives/datamap.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('datamap', function () { 4 | 5 | var $compile, $rootScope; 6 | 7 | beforeEach(function () { 8 | angular.mock.module('datamaps'); 9 | 10 | inject(function ($injector) { 11 | $compile = $injector.get('$compile'); 12 | $rootScope = $injector.get('$rootScope'); 13 | }); 14 | }); 15 | 16 | it('should display datamap', function () { 17 | var element = $compile('')($rootScope); 18 | $rootScope.$digest(); 19 | expect(element.html()).toContain('svg'); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /usaMap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmachat/angular-datamaps/1813d387a7d32bf59c353455c06b1b2ff62f06e1/usaMap.png --------------------------------------------------------------------------------