I'm trying to use the angularjs tooltip directive on my d3 visualisation, so I have something like
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("circle")
.attr("tooltip-append-to-body", true)
.attr("tooltip", function(d) {
return d.name;
})
// ... attributes
However, the tooltips are not showing. Do I need to $compile
or something? I've tried wrapping it around $timeout
too, but that didn't work.
I had a similar problem and yes, solved it with $compile
. I'm assuming your d3 code is inside a custom directive. From there you can add your tooltip attributes, remove your custom directive attribute so $compile only runs once, and call $compile:
myApp.directive('myNodes', ['$compile', function ($compile) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var nodes = [{"name": "foo"}, {"name": "bar"}]
var mySvg = d3.select(element[0])
.append("svg")
.attr("width", 100)
.attr("height", 100);
var node = mySvg.selectAll(".node")
.data(nodes)
.enter()
.append("circle")
.attr("cx", function(d,i){
return 20+i*50;
})
.attr("cy", 50)
.attr("r", 10)
.attr("tooltip-append-to-body", true)
.attr("tooltip", function(d){
return d.name;
});
element.removeAttr("my-nodes");
$compile(element)(scope);
}
};
}]);
The $compile service makes sure your element is compiled with the attributes added by your directive.
Here is a working fiddle using the above code. Hope it's what you're looking for!
A pretty good answer from @jbll - But it will probably be best to chain the directive compilation onto the end of the enter phase. It is important to have an enter phase and an update phase so the graphic can respond to data updates without recreating every element. The previous answer would have every directive on every node compiled whenever the model was changed. This may be what is wanted, but probably not.
The following code shows the d3 graphic updating whenever the $scope.nodes variable changes.
This is also a little neater because it doesn't require the removal and recreation of the original directive, which seems like a bit of a hack.
Here is the Fiddle
Add the button to the html:
<button ng-click="moveDots()">Move the dots</button>
And then change the JavaScript fie to:
var myApp = angular.module('myApp', ['ui.bootstrap']);
myApp.controller('myCtrl', ['$scope', function($scope){
$scope.nodes = [
{"name": "foo", x: 50, y: 50},
{"name": "bar", x: 100, y: 100}
];
$scope.moveDots = function(){
for(var n = 0; n < $scope.nodes.length; n++){
var node = $scope.nodes[n];
node.x = Math.random() * 200 + 20;
node.y = Math.random() * 200 + 20;
}
}
}]);
myApp.directive('myNodes', ['$compile', function ($compile) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var mySvg = d3.select(element[0])
.append("svg")
.attr("width", 250)
.attr("height", 250);
renderDots();
scope.$watch("nodes", renderDots, true);
function renderDots(){
// ENTER PHASE
mySvg.selectAll("circle")
.data(scope.nodes)
.enter()
.append("circle")
.attr("tooltip-append-to-body", true)
.attr("tooltip", function(d){
return d.name;
})
.call(function(){
$compile(this[0].parentNode)(scope);
});
// UPDATE PHASE - no call to enter(nodes) so all circles are selected
mySvg.selectAll("circle")
.attr("cx", function(d,i){
return d.x;
})
.attr("cy", function(d,i){
return d.y;
})
.attr("r", 10);
// todo: EXIT PHASE (remove any elements with deleted data)
}
}
};
}]);
if the html is generated by something other than angularjs and inserted into the DOM than you will need to compile the html that includes your directive attributes prior to incerting it into the DOM so that angular knows about it.
I like this method much better since you don't have to call removeAttr (seems like a hack)
myApp.directive('myNodes', ['$compile', function ($compile) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var nodes = [{"name": "foo"}, {"name": "bar"}]
var mySvg = d3.select(element[0])
.append("svg")
.attr("width", 100)
.attr("height", 100);
var node = mySvg.selectAll(".node")
.data(nodes)
.enter()
.append("circle")
.attr("cx", function(d,i){
return 20+i*50;
})
.attr("cy", 50)
.attr("r", 10)
.attr("tooltip-append-to-body", true)
.attr("tooltip", function(d){
return d.name;
});
$compile(svg[0])(scope);
}
};
}]);
@david004 makes a good point about chaining onto enter()
, so $compile
is only called once on each entering element. But instead of calling $compile
on the parentNode
, here's how I'm using call to $compile
each individual entering element:
// Entering
myD3Selection.enter()
.append( 'rect' )
.attr( {foo: 'bar'} )
.call( compile );
// Compile encapsulated in reusable function, so can be used on multiple enter() chains
function compile( d3Selection )
{
d3Selection.each( function( d, i )
{
// this is the actual DOM element
$compile( this )( scope );
} );
}