├── .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 | 
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 = ['
'],
132 | label = '';
133 | for (var fillKey in this.options.fills) {
134 | html.push('- ',
136 | fillKey,
137 | '
');
138 | }
139 | html.push('
');
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 |
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
--------------------------------------------------------------------------------