I'm trying to create my first app with AngularJS. It looks neat, but there's a lot of abstraction, and I'm just curious if anyone has advice on the most idiomatic way to use the angular methodology to update visuals created with d3js.
Thanks, bp
Please also check out the article by Brian Ford (AngularJS intern) where he describes integrating AngluarJS with D3 in detail.
In order to make angular and other frameworks play nice is to wrap the "other" frameworks using directives.
http://docs.angularjs.org/guide/directive
The thing that you want to do is to tell angular when data has been updated by the "other" frameworks. If angular doesn't need to know, then your task is simpler.
Here is an example that works with SVG, its awesome
http://sullerandras.github.com/SVG-Sequence-Diagram/
Here is an example that wraps TinyMCE
There is also the possibility to insert the AngularJS handle bar syntax directly into the d3 generated elements:
var containerDiv = d3.select(targetCSSSelectorForADiv);
var svgG = containerDiv
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
svgG.selectAll(".tempclass").data(scope.circles).enter()
.append("circle")
.attr("class", "tempclass")
.attr("cx", function (d, i) { return "{{circles[" + i + "].cx}}" })
.attr("cy", function (d, i) { return "{{circles[" + i + "].cy}}" })
.attr("r", function (d, i) { return "{{circles[" + i + "].radius}}" })
.attr("ng-style", function (d, i)
{
return "{fill: circles[" + i + "].circolor"
+ ", opacity: circles[" + i + "].opa"
+ ", 'stroke-width': 4*circles[" + i + "].opa"
+ ", stroke: 'red' }";
});
Please note the following things: the scope is in-fact the angular scope object passed down from the directive to the rendering function. Setting the style of an element to an "{{...}}" expression will not work so I am using the "ng-style" attribute here.
However there is one more trick: You need to tell Angular to look at the dynamically generated DOM elements and wire up the data binding, I know now of two ways of doing this:
//the target div is the one with the angular ng-controller attribute
//this you can call at the end of the d3 rendering call from within the render function
angular.bootstrap(document.getElementById("d3ContainerDivID"), ['d3App']);
the other way is this:
//and this could be called from the directive that triggered the rendering or
//some other place that could have the angular $compile service injected
$compile(document.getElementById("d3ContainerDivID"))(scope);
Now you can change your scope members and they will be directly updated to your d3 elements, in this case the svg circles. In the angular controller (which gets instantiated before the directive fires that draws the d3 objects).
$scope.circles = [];
for (var i = 0; i < 50; i++)
{
$scope.circles.push(new Circle());
}
setInterval(function ()
{
$scope.circles.forEach(function (d, i) { $scope.circles[i] = new Circle(); });
$scope.$digest();
}, 2000);
Please note the $digest call, which tells angular to digest the changed scope; this will change the values on the svg circle elements. For anything like animations and such, d3 is now not responsible anymore and one would have to implement manually or use a different pattern.
You can also follow along with this tutorial/screencast to see how to use D3 with angular. It's a bit different, because it utilizes a wrapper library around d3 called rickshaw, which provides some graphing specific stuff, but the approach is exactly the same:
If we use d3 inside a directive to generate elements with other Angular directives (as I think you will find is quite a common requirement) you can call $compile
on the end of the UPDATE phase of the rendering process with the call()
method. Like this (assuming we're rendering a bunch of circles):
mySvg.selectAll("circle")
.data(scope.nodes)
.enter()
.append("circle")
.attr("someDirective")
.call(function(){
$compile(this[0].parentNode)(scope);
});