From 4e484c81469ff04573ecf85d3c4f6ceefe99690b Mon Sep 17 00:00:00 2001 From: Drew Machat Date: Mon, 25 May 2015 22:15:24 -0400 Subject: [PATCH] Responsive, choropleth animations, readme and changelog --- CHANGELOG.md | 16 ++++ README.md | 6 ++ bower.json | 4 +- dev/index.html | 9 ++- dist/angular-datamaps.js | 112 ++++++++++++++++++++------- dist/angular-datamaps.min.js | 2 +- src/directives/datamaps-directive.js | 44 ++++++++--- 7 files changed, 147 insertions(+), 46 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b566abd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +## 0.1.0 +###### _May 25, 2015_ + +##### Breaking Changes +- Refactored scope API variables + - `scope.map` is now the only required input, and maps directly to the object required by Datamaps + +##### General +- Updated Datamaps dependency to v0.4.0 +- Remove unneccessary non-semantic markup hindering Datamap rendering +- API for loading plugins for Datamaps has been added + - Example plugin with a custom legend included in Readme +- Responsive binding has been added +- Zoomable option has been added +- Allow updateChoropleth when geographies don't change +- Slightly cleaner watch diff --git a/README.md b/README.md index ea92131..4d3fb95 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,12 @@ $scope.updateActiveGeography = function(geography) { ### Toggle zoom ### Set the `zoomable` attribute to toggle a simple zoom on the map. +### Responsive ### +Bind the built-in Datamaps responsive methods by setting `$scope.mapObject.responsive = true`. + +### Animated Update Choropleth ### +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. + ### Adding plugins ### 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. diff --git a/bower.json b/bower.json index 4ffec4c..cda744b 100644 --- a/bower.json +++ b/bower.json @@ -1,12 +1,12 @@ { "name": "angular-datamaps", "description": "AngularJS Datamaps -- provides an Angular directive to wrap https://github.com/markmarkoh/datamaps", - "version": "0.0.4", + "version": "0.0.5", "author": "Drew Machat", "main": "dist/angular-datamaps.min.js", "dependencies": { "angular": ">=1.0.8", - "datamaps": "~0.3.2" + "datamaps": "~0.4.0" }, "devDependencies": { "es5-shim": "~2.1.0", diff --git a/dev/index.html b/dev/index.html index d78f828..96d2d25 100644 --- a/dev/index.html +++ b/dev/index.html @@ -41,13 +41,18 @@ html.push(''); d3.select(this.options.element).append('div') .attr('class', 'datamaps-legend') + .style('position', 'absolute') + .style('bottom', 0) .html(html.join('')); } }; $scope.map = { scope: 'usa', - options: {}, + responsive: true, + options: { + staticGeoData: true + }, geographyConfig: { highlightBorderColor: '#bada55', popupTemplate: function(geography, data) { @@ -68,7 +73,7 @@ 'Light Republican': '#EAA9A8', defaultFill: '#b9b9b9' }, - data:{ + data: { "AZ": { "fillKey": "Republican", "electoralVotes": 5 diff --git a/dist/angular-datamaps.js b/dist/angular-datamaps.js index b877f11..a655127 100644 --- a/dist/angular-datamaps.js +++ b/dist/angular-datamaps.js @@ -1,77 +1,111 @@ 'use strict'; + angular.module('datamaps', []); + 'use strict'; -angular.module('datamaps').directive('datamap', [ - '$compile', - function ($compile) { + +angular + + .module('datamaps') + + .directive('datamap', ['$window', function($window) { return { restrict: 'EA', scope: { - map: '=', - plugins: '=?', - zoomable: '@?', - onClick: '&?' + map: '=', //datamaps objects [required] + plugins: '=?', //datamaps plugins [optional] + zoomable: '@?', //zoomable toggle [optional] + onClick: '&?', //geography onClick event [optional] }, - link: function (scope, element, attrs) { + link: function(scope, element, attrs) { + // Generate base map options function mapOptions() { return { - element: element[0].children[0], + element: element[0], scope: 'usa', height: scope.height, width: scope.width, - fills: { defaultFill: '#b9b9b9' }, + fills: { + defaultFill: '#b9b9b9' + }, data: {}, - done: function (datamap) { + done: function(datamap) { function redraw() { - datamap.svg.selectAll('g').attr('transform', 'translate(' + d3.event.translate + ')scale(' + d3.event.scale + ')'); + datamap.svg.selectAll('g') + .attr('transform', 'translate(' + d3.event.translate + ')scale(' + d3.event.scale + ')'); } if (angular.isDefined(attrs.onClick)) { - datamap.svg.selectAll('.datamaps-subunit').on('click', function (geography) { + datamap.svg.selectAll('.datamaps-subunit').on('click', function(geography) { scope.onClick()(geography); }); } if (angular.isDefined(attrs.zoomable)) { - datamap.svg.call(d3.behavior.zoom().on('zoom', redraw)); + datamap.svg.call(d3.behavior.zoom() + .on('zoom', redraw)); } } }; } + scope.api = { - refresh: function (map) { + + // Fully refresh directive + refresh: function(map) { scope.api.updateWithOptions(map); }, - updateWithOptions: function (map) { + + // Update chart with new options + updateWithOptions: function(map) { + // Clearing scope.api.clearElement(); + // Update bounding box scope.width = (map.options || {}).width || null; scope.height = (map.options || {}).height || (scope.width ? scope.width * 0.5 : null); scope.legendHeight = (map.options || {}).legendHeight || 50; + // Set a few defaults for the directive scope.mapOptions = mapOptions(); + // Add the good stuff scope.mapOptions = angular.extend(scope.mapOptions, map); + scope.datamap = new Datamap(scope.mapOptions); + + // Add responsive listeners + if (scope.mapOptions.responsive) { + $window.addEventListener('resize', scope.api.resize); + } else { + $window.removeEventListener('resize', scope.api.resize); + } + // Update plugins scope.api.updatePlugins(scope.datamap); + // Update options and choropleth scope.api.refreshOptions(map.options); scope.api.updateWithData(map.data); }, - updatePlugins: function (datamap) { + + // Add and initialize optional plugins + updatePlugins: function(datamap) { if (!scope.plugins) { return; } - angular.forEach(scope.plugins, function (plugin, name) { + angular.forEach(scope.plugins, function(plugin, name) { datamap.addPlugin(name, plugin); datamap[name](); }); }, - refreshOptions: function (options) { + + // Set options on the datamap + refreshOptions: function(options) { if (!options) { return; } + // set labels if (options.labels) { scope.datamap.labels({ @@ -79,36 +113,54 @@ angular.module('datamaps').directive('datamap', [ fontSize: options.labelSize ? options.labelSize : 12 }); } + // set legend if (options.legend) { scope.datamap.legend(); } }, - updateWithData: function (data) { + + // Trigger datamaps resize method + resize: function() { + console.log('resize attempt'); + scope.datamap.resize(); + }, + + // Update chart with new data + updateWithData: function(data) { scope.datamap.updateChoropleth(data); scope.api.updatePlugins(scope.datamap); }, + + // Fully clear directive element clearElement: function () { scope.datamap = null; - element.empty(); - var mapContainer = $compile('
')(scope); - element.append(mapContainer); + element + .empty() + .css({ + 'position': 'relative', + 'display': 'block', + 'padding-bottom': scope.legendHeight + 'px' + }); } }; + // Watch data changing - scope.$watch('map', function (map, old) { + scope.$watch('map', function(map, old) { // Return if no data - if (angular.isUndefined(map) || angular.equals({}, map)) { + if (!map || angular.equals({}, map)) { return; } - // Init the datamap, or update data - if (!scope.datamap || angular.equals(old.data, map.data)) { + // Allow animated transition when geos don't change + // or fully refresh + if (!scope.datamap || angular.equals(map.data, old.data)) { scope.api.refresh(map); - } else { + } else if ((map.options || {}).staticGeoData) { scope.api.updateWithData(map.data); + } else { + scope.api.refresh(map); } }, true); } }; - } -]); \ No newline at end of file + }]); diff --git a/dist/angular-datamaps.min.js b/dist/angular-datamaps.min.js index 03462f5..43ea69c 100644 --- a/dist/angular-datamaps.min.js +++ b/dist/angular-datamaps.min.js @@ -1 +1 @@ -"use strict";angular.module("datamaps",[]),angular.module("datamaps").directive("datamap",["$compile",function(a){return{restrict:"EA",scope:{map:"=",plugins:"=?",zoomable:"@?",onClick:"&?"},link:function(b,c,d){function e(){return{element:c[0].children[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(a){b.api.clearElement(),b.width=(a.options||{}).width||null,b.height=(a.options||{}).height||(b.width?.5*b.width:null),b.legendHeight=(a.options||{}).legendHeight||50,b.mapOptions=e(),b.mapOptions=angular.extend(b.mapOptions,a),b.datamap=new Datamap(b.mapOptions),b.api.updatePlugins(b.datamap),b.api.refreshOptions(a.options),b.api.updateWithData(a.data)},updatePlugins:function(a){b.plugins&&angular.forEach(b.plugins,function(b,c){a.addPlugin(c,b),a[c]()})},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())},updateWithData:function(a){b.datamap.updateChoropleth(a),b.api.updatePlugins(b.datamap)},clearElement:function(){b.datamap=null,c.empty();var d=a('
')(b);c.append(d)}},b.$watch("map",function(a,c){angular.isUndefined(a)||angular.equals({},a)||(!b.datamap||angular.equals(c.data,a.data)?b.api.refresh(a):b.api.updateWithData(a.data))},!0)}}}]); \ No newline at end of file +"use strict";angular.module("datamaps",[]),angular.module("datamaps").directive("datamap",["$compile",function(a){return{restrict:"EA",scope:{map:"=",plugins:"=?",zoomable:"@?",onClick:"&?"},link:function(b,c,d){function e(){return{element:c[0].children[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(a){b.api.clearElement(),b.width=(a.options||{}).width||null,b.height=(a.options||{}).height||(b.width?.5*b.width:null),b.legendHeight=(a.options||{}).legendHeight||50,b.mapOptions=e(),b.mapOptions=angular.extend(b.mapOptions,a),b.datamap=new Datamap(b.mapOptions),b.api.updatePlugins(b.datamap),b.api.refreshOptions(a.options),b.api.updateWithData(a.data)},updatePlugins:function(a){b.plugins&&angular.forEach(b.plugins,function(b,c){a.addPlugin(c,b),a[c]()})},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(),a.responsive?window.addEventListener("resize",b.api.resize):window.removeEventListener("resize",b.api.resize))},resize:function(){b.datamap.resize()},updateWithData:function(a){b.datamap.updateChoropleth(a),b.api.updatePlugins(b.datamap)},clearElement:function(){b.datamap=null,c.empty();var d=a('
')(b);c.append(d)}},b.$watch("map",function(a,c){angular.isUndefined(a)||angular.equals({},a)||(b.datamap&&angular.equals(c,a)?b.api.updateWithData(a.data):b.api.refresh(a))},!0)}}}]); \ No newline at end of file diff --git a/src/directives/datamaps-directive.js b/src/directives/datamaps-directive.js index 4cc8f57..f2b6160 100644 --- a/src/directives/datamaps-directive.js +++ b/src/directives/datamaps-directive.js @@ -1,14 +1,16 @@ 'use strict'; -angular.module('datamaps') +angular - .directive('datamap', ['$compile', function($compile) { + .module('datamaps') + + .directive('datamap', ['$window', function($window) { return { restrict: 'EA', scope: { map: '=', //datamaps objects [required] plugins: '=?', //datamaps plugins [optional] - zoomable: '@?', //zoomable toggle [optional] + zoomable: '@?', //zoomable toggle [optional] onClick: '&?', //geography onClick event [optional] }, link: function(scope, element, attrs) { @@ -16,7 +18,7 @@ angular.module('datamaps') // Generate base map options function mapOptions() { return { - element: element[0].children[0], + element: element[0], scope: 'usa', height: scope.height, width: scope.width, @@ -68,6 +70,13 @@ angular.module('datamaps') scope.datamap = new Datamap(scope.mapOptions); + // Add responsive listeners + if (scope.mapOptions.responsive) { + $window.addEventListener('resize', scope.api.resize); + } else { + $window.removeEventListener('resize', scope.api.resize); + } + // Update plugins scope.api.updatePlugins(scope.datamap); @@ -107,6 +116,12 @@ angular.module('datamaps') } }, + // Trigger datamaps resize method + resize: function() { + console.log('resize attempt'); + scope.datamap.resize(); + }, + // Update chart with new data updateWithData: function(data) { scope.datamap.updateChoropleth(data); @@ -116,23 +131,30 @@ angular.module('datamaps') // Fully clear directive element clearElement: function () { scope.datamap = null; - element.empty(); - var mapContainer = $compile('
')(scope); - element.append(mapContainer); + element + .empty() + .css({ + 'position': 'relative', + 'display': 'block', + 'padding-bottom': scope.legendHeight + 'px' + }); } }; // Watch data changing scope.$watch('map', function(map, old) { // Return if no data - if (angular.isUndefined(map) || angular.equals({}, map)) { + if (!map || angular.equals({}, map)) { return; } - // Init the datamap, or update data - if (!scope.datamap || angular.equals(old.data, map.data)) { + // Allow animated transition when geos don't change + // or fully refresh + if (!scope.datamap || angular.equals(map.data, old.data)) { scope.api.refresh(map); - } else { + } else if ((map.options || {}).staticGeoData) { scope.api.updateWithData(map.data); + } else { + scope.api.refresh(map); } }, true); }