Angular directive for D3 datamap rendering only once - angularjs

So, I made an Angular directive which renders a D3 datamap in an HTML template. I pass the data to the directive via a 'data' attribute. The problem I am facing is that the map displays perfectly when the page is loaded for the first time. However, when I come back to the template by navigating from other templates (routing done through 'ui-route'), the map doesn't get rendered and there is no error in the console either. Here's my directive:
app.directive('stabilityMap', function() {
var containerid = document.getElementById('world-map-container');
var margin = 20,
padding = 50,
width = containerid.offsetWidth - margin;
height = containerid.offsetHeight - margin;
return {
restrict: 'A',
scope: {
data: '=',
},
link : function(scope, element, attrs) {
scope.$watch('data', function(newVal, oldVal) {
var colorScale = d3.scale.linear().domain([50, 100]).range(['#ff0000', '#280000']);
var Fills = {defaultFill: '#ddd'};
var Data = {};
var countries = Datamap.prototype.worldTopo.objects.world.geometries;
for(var i = 0; i < newVal.length; i++) {
for (var j = 0; j < countries.length; j++) {
if(countries[j].properties.name == newVal[i].Country) {
Fills[countries[j].id] = colorScale(newVal[i]['Stability Index']);
Data[countries[j].id] = { fillKey : countries[j].id};
}
}
}
var map = new Datamap({
element: containerid,
responsive: true,
projection: 'mercator',
setProjection: function(element) {
var projection = d3.geo.mercator()
.center([0, padding])
.scale(105)
.translate([element.offsetWidth / 2, element.offsetHeight / 2 - 70]);
var path = d3.geo.path()
.projection(projection);
return {path: path, projection: projection};
},
fills: Fills,
data: Data
})
d3.select(window).on('resize', function() {
map.resize();
});
})
}
}
})
Here's my angular controller for the template:
app.controller('CountryCtrl', ['$scope', '$http', function($scope, $http) {
$scope.countriesData = [
{'Country': 'Australia', 'Stability Index':'85'},
{'Country':'United States of America', 'Stability Index':'90'},
{'Country':'Russia', 'Stability Index':'70'},
{'Country':'India', 'Stability Index':'84.2'},
{'Country':'China', 'Stability Index':'50'}
]
}]);
Here's the HTML template:
<div class="row" id="world-map">
<div stability-map data="countriesData" id="world-map-container">
</div>
</div>
Here is the screenshot when the page is loaded first:
And the empty container after I come back to the page after navigating from some other template of the website.
Any idea what's happening?

the map doesnt get rendered again, because the function is only run once, when the code is first executed. I suggest you wrap your creation of the map inside a function and call that function every time you load the page (from the controller).
$scope.renderMap = function() {
var map = new Datamap({
element: containerid,
responsive: true,
projection: 'mercator',
setProjection: function(element) {
var projection = d3.geo.mercator()
.center([0, padding])
.scale(105)
.translate([element.offsetWidth / 2, element.offsetHeight / 2 - 70]);
var path = d3.geo.path()
.projection(projection);
return {path: path, projection: projection};
},
fills: Fills,
data: Data
})
}
The code you will then need to render the map is the following:
$scope.renderMap()
I hope this helps.
xoxo

I found the answer to this. You have to put your variables inside your link function. This means moving
var containerid = document.getElementById('world-map-container');
var margin = 20,
padding = 50,
width = containerid.offsetWidth - margin;
height = containerid.offsetHeight - margin;
right inside your link function. This solved the problem for me.

Related

Angular : load lots of data via directive causing browser issue

I have an angular directive loading a svg map (using amchart) where I add thousands of svg circles. In the end everything works but my browser seems in pain and I would need to (1) optimize my loading and (2) display a loading symbol that could last till the map can actually display for real.
Today I use this kind of directive attribute to know when my directive is loaded :
directive('initialisation',['$rootScope',function($rootScope) {
return {
restrict: 'A',
link: function($scope) {
var to;
var listener = $scope.$watch(function() {
clearTimeout(to);
to = setTimeout(function () {
listener();
$rootScope.$broadcast('initialised');
}, 50);
});
}
};
Well this is not good to me as my loading symbol (angular-material) freezes, and then disappear to leaves an empty browser for a few seconds, before the map can render. For information I use ng-hide on the loading div and ng-show on the map div, and this is the way I apply it :
$scope.$on('initialised', function() {
$scope.$apply(function(){
$scope.mapLoaded = true;
});
})
Do you know a way to solve my (1) and (2) issue ? Or should I look for another js library to do this?
Thank you
PS : here is my map directive (images is an array with 20k entry at the moment) :
directive('amChartsLanguage', function() {
return {
restrict: 'E',
replace:true,
template: '<div id="mapLanguage" style="height: 1000px; margin: 0 auto"> </div>',
link: function(scope, element, attrs) {
var chart = false;
var initChart = function() {
if (chart) chart.destroy();
var images = [];
var legendData = [];
for(var i=0 ; i < scope.languageZeppelin.length ; i ++ ) {
images.push( {
"type": "circle",
"width": 7,
"height": 7,
"color": scope.languageZeppelin[i].color,
"longitude": scope.languageZeppelin[i].lon,
"latitude": scope.languageZeppelin[i].lat
} );
}
var legend = new AmCharts.AmLegend();
legend.width="10%";
legend.height="300";
legend.equalWidths = false;
legend.backgroundAlpha = 0.5;
legend.backgroundColor = "#FFFFFF";
legend.borderColor = "#ffffff";
legend.borderAlpha = 1;
legend.verticalGap = 10;
legend.top = 150;
legend.left = 70;
legend.position = "left";
legend.maxColumns = 1;
legend.data = scope.legend;
// build map
chart = AmCharts.makeChart( "mapLanguage", {
"type": "map",
"areasSettings": {
"unlistedAreasColor": "#15A892",
"autoZoom": true,
"selectedColor": "#FFCC00",
"color": "#909090"
},
"dataProvider": {
"map": "worldLow",
"getAreasFromMap": true,
"images": images,
"zoomLevel": 1,
"zoomLongitude": 6,
"zoomLatitude": 11
},
"export": {
"enabled": false
}
} );
chart.addLegend(legend);
chart.validateNow(legend);
};
initChart();
}
}
})
we have angular with a lot more data on the page. with poor design og architechture. loading ofcoarse too long but after that performance is great. we use d3

How to get async html attribut

I have a list of items retreived by an async call and the list is shown with the help of ng-repeat. Since the div container of that list has a fixed height (400px) I want the scrollbar to be at the bottom. And for doing so I need the scrollHeight. But the scrollHeight in postLink is not the final height but the initial height.
Example
ppChat.tpl.html
<!-- Height of "chatroom" is "400px" -->
<div class="chatroom">
<!-- Height of "messages" after all messages have been loaded is "4468px" -->
<div class="messages" ng-repeat="message in chat.messages">
<chat-message data="message"></chat-message>
</div>
</div>
ppChat.js
// [...]
compile: function(element) {
element.addClass('pp-chat');
return function(scope, element, attrs, PpChatController) {
var messagesDiv;
// My idea was to wait until the messages have been loaded...
PpChatController.messages.$loaded(function() {
// ...and then recompile the messages div container
messagesDiv = $compile(element.children()[0])(scope);
// Unfortunately this doesn't work. "messagesDiv[0].scrollHeight" still has its initial height of "400px"
});
}
}
Can someone explain what I missed here?
As required here is a plunk of it
You can get the scrollHeight of the div after the DOM is updated by doing it in the following way.
The below directive sets up a watch on the array i.e. a collection, and uses the $timeout service to wait for the DOM to be updated and then it scrolls to the bottom of the div.
chatDirective.$inject = ['$timeout'];
function chatDirective($timeout) {
return {
require: 'chat',
scope: {
messages: '='
},
templateUrl: 'partials/chat.tpl.html',
bindToController: true,
controllerAs: 'chat',
controller: ChatController,
link: function(scope, element, attrs, ChatController) {
scope.$watchCollection(function () {
return scope.chat.messages;
}, function (newValue, oldValue) {
if (newValue.length) {
$timeout(function () {
var chatBox = document.getElementsByClassName('chat')[0];
console.log(element.children(), chatBox.scrollHeight);
chatBox.scrollTop = chatBox.scrollHeight;
});
}
});
}
};
}
The updated plunker is here.
Also in your Controller you have written as,
var Controller = this;
this.messages = [];
It's better to write in this way, here vm stands for ViewModel
AppController.$inject = ['$timeout'];
function AppController($timeout) {
var vm = this;
vm.messages = [];
$timeout(
function() {
for (var i = 0; i < 100; i++) {
vm.messages.push({
message: getRandomString(),
created: new Date()
});
}
},
3000
);
}

Angularjs responsive directive live updating issue (possibly due to ng-repeating the directive)

I am creating a post feed by ng-repeating JSON files from the cloud. I tried to make the posts responsive by using angular directives that update the template url with the screen size.
The problem is that only the last post in the ng-repeat responds and changes templates (with or without the reverse filter) when I resize the page. The other posts just remain the template that it was when originally loaded.
Here's the ng-repeat in the page
<div ng-show="post_loaded" ng-repeat="post in posts | reverse | filter:searchText ">
<feed-post>
</feed-post>
</div>
Here's the directive javascript file
app.directive('feedPost', function ($window) {
return {
restrict: 'E',
template: '<div ng-include="templateUrl"></div>',
link: function(scope) {
$window.onresize = function() {
changeTemplate();
scope.$apply();
};
changeTemplate();
function changeTemplate() {
var screenWidth = $window.innerWidth;
if (screenWidth < 768) {
scope.templateUrl = 'directives/post_mobile.html';
} else if (screenWidth >= 768) {
scope.templateUrl = 'directives/post_desktop.html';
}
}
}
};});
This happens because you re-assigning the .onresize in each directive and it stays effective only for the last linked directive.
I'd suggest to use it in a more angular way. You don't actually need a custom directive
In the controller that manages list of posts add reference to $window in $scope
$scope.window = $window;
Then in template make use of it
<div ng-include="directives/post_mobile.html" ng-if="window.innerWidth < 768"></div>
<div ng-include="directives/post_desktop.html" ng-if="window.innerWidth >= 768"></div>
To avoid extra wrappers for posts feed you might want to use ng-repeat-start, ng-repeat-end directives
this is a directive i wrote based on bootstrap sizes and ngIf directive :
mainApp.directive("responsive", function($window, $animate) {
return {
restrict: "A",
transclude: 'element',
terminal: true,
link: function($scope, $element, $attr, ctrl, $transclude) {
//var val = $attr["responsive"];
var block, childScope;
$scope.$watch(function(){ return $window.innerWidth; }, function (width) {
if (width < 768) {
var s = "xs";
} else if (width < 992) {
var s = "sm";
} else if (width < 1200) {
var s = "md";
} else {
var s = "lg";
}
console.log("responsive ok?", $attr.responsive == s);
if ($attr.responsive == s) {
if (!childScope) {
$transclude(function(clone, newScope) {
childScope = newScope;
clone[clone.length++] = document.createComment(' end responsive: ' + $attr.responsive + ' ');
block = {
clone: clone
};
$animate.enter(clone, $element.parent(), $element);
});
}
} else {
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
block.clone.remove();
block.clone = null;
block = null;
}
}
});
}
};
});

Drawing morris chart in angular directive almost shows up

I'm trying to draw a morris chart in an angular directive that is within an ng-repeat block. It is weird, because it draws, almost? I can see it's there and the mouseovers work, but the graph itself is only a thin line at the top. Does anybody have any ideas?
Here's the html:
<div id="page-wrapper" ng-repeat="d in dealerGroup.Dealerships__r" ng-if="expandedDealer == d.Id">
<div class="panel-heading">Area Chart Example</div>
<div class="panel-body">
<area-chart dealership="d" chartData="d.SalesChartData"></area-chart>
</div>
</div>
And here's the directive
angular.module('areaChart', ['ui.bootstrap']).directive('areaChart', function($window) {
var directive = {};
// directive.templateUrl = directivePath + '/charts/area-chart.html';
directive.restrict = 'EA';
directive.scope = {
dealership: "=",
chartdata: "="
};
directive.controller = function($scope) {
$scope.ykeys = function() {
var ykeys = [];
angular.forEach($scope.chartdata, function(d,k) {
angular.forEach(d, function(value,key) {
if(key != 'period') { ykeys.push(key); }
})
});
return ykeys;
}
}
directive.link = function($scope,element,attrs) {
Morris.Area({
element: element,
xkey: 'period',
ykeys: $scope.ykeys(),
labels: $scope.ykeys(),
hideHover: 'auto',
pointSize: 2,
data: $scope.chartdata
});
}
return directive;
});
And here's what happens:
Additionally, resizing makes the whole thing blow up with javascript errors everywhere. But i'll worry that separately

AngularJS Passing Variable to Directive

I'm new to angularjs and am writing my first directive. I've got half the way there but am struggling figuring out how to pass some variables to a directive.
My directive:
app.directive('chart', function () {
return{
restrict: 'E',
link: function (scope, elem, attrs) {
var chart = null;
var opts = {};
alert(scope[attrs.chartoptions]);
var data = scope[attrs.ngModel];
scope.$watch(attrs.ngModel, function (v) {
if (!chart) {
chart = $.plot(elem, v, opts);
elem.show();
} else {
chart.setData(v);
chart.setupGrid();
chart.draw();
}
});
}
};
});
My controller:
function AdListCtrl($scope, $http, $rootScope, $compile, $routeParams, AlertboxAPI) {
//grabing ad stats
$http.get("/ads/stats/").success(function (data) {
$scope.exports = data.ads;
if ($scope.exports > 0) {
$scope.show_export = true;
} else {
$scope.show_export = false;
}
//loop over the data
var chart_data = []
var chart_data_ticks = []
for (var i = 0; i < data.recent_ads.length; i++) {
chart_data.push([0, data.recent_ads[i].ads]);
chart_data_ticks.push(data.recent_ads[i].start);
}
//setup the chart
$scope.data = [{data: chart_data,lines: {show: true, fill: true}}];
$scope.chart_options = {xaxis: {ticks: [chart_data_ticks]}};
});
}
My Html:
<div class='row-fluid' ng-controller="AdListCtrl">
<div class='span12' style='height:400px;'>
<chart ng-model='data' style='width:400px;height:300px;display:none;' chartoptions="chart_options"></chart>
{[{ chart_options }]}
</div>
</div>
I can access the $scope.data in the directive, but I can't seem to access the $scope.chart_options data.. It's definelty being set as If I echo it, it displays on the page..
Any ideas what I'm doing wrong?
UPDATE:
For some reason, with this directive, if I move the alert(scope[attrs.chartoptions]); to inside the $watch, it first alerts as "undefined", then again as the proper value, otherwise it's always undefined. Could it be related to the jquery flot library I'm using to draw the chart?
Cheers,
Ben
One problem I see is here:
scope.$watch(attrs.ngModel, function (v) {
The docs on this method are unfortunately not that clear, but the first argument to $watch, the watchExpression, needs to be an angular expression string or a function. So in your case, I believe that you need to change it to:
scope.$watch("attrs.ngModel", function (v) {
If that doesn't work, just post a jsfiddle or jsbin.com with your example.

Resources