25

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.

Tom
  • 26,212
  • 21
  • 100
  • 111
zlog
  • 3,316
  • 4
  • 42
  • 82
  • Have you tried adding a tooltip to a svg-circle w/o any d3? You may need to wrap it inside a foreign object. – Adam Pearce Dec 05 '13 at 14:06
  • Yes, I have it working with d3 and plain bootstrap ui without any additional elements. Can't figure out how to do it with angular-ui though. – zlog Dec 05 '13 at 14:37
  • 1
    If you are using `angular` to add the HTML to the DOM (via a directive), then this answer should help: http://stackoverflow.com/questions/19656365/angular-ui-d3-how-to-implement-contextual-menu-popover-vs-modal/19663183#19663183 Post some more code about how this DOM element is getting added. – musically_ut Dec 10 '13 at 06:28
  • 1
    I've encountered these articles that touch on the issue, but it seems like d3 and angularjs might need some work deep down for them to be compatible: http://alexandros.resin.io/angular-d3-svg/, http://www.ng-newsletter.com/posts/d3-on-angular.html. Will need some more exploration... – zlog Dec 11 '13 at 11:43

5 Answers5

19

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!

jbll
  • 1,156
  • 1
  • 11
  • 14
  • I had my d3 code in the directive controller, instead of the link function, so it's more like: http://jsfiddle.net/FBFR6/3/, but thanks for the pointer on how to use `$compile`! – zlog Jan 27 '14 at 13:20
  • Note also that it's important to add `element.removeAttr("my-nodes");`, so that `$compile` doesn't recompile the directive and remove the d3 nodes. Also, this works best as an attribute rather than an element, so you can remove and add the directive as needed. – zlog Jan 27 '14 at 15:37
  • 1
    where does `element` come from as the argument to this function, its unclear how it is being selected, although it seems to work correctly.. – qwwqwwq Feb 09 '14 at 19:31
  • answer my own question: While declaring an AngularJS directive, the naming convention followed is camelCase. For example, we would define the name of the directive as ‘fundooDatepicker’. But when you actually use the directive in your HTML, it is the dash-separated version. That is, our widget would be ‘’ and not ‘ – qwwqwwq Feb 09 '14 at 19:34
  • I used your solution of using $compile(element)(scope) to make this work in my own chart, but I have an issue where I use transitions to change the data. In that case, I need to change the tooltip. But when I recompile, multiple tooltips now pop up. Do you have any suggestion on how to avoid this? The $tooltip provider doesn't seem to have a destroy method. – Richard Bender Mar 21 '14 at 21:42
  • @RichardBender do you have a fiddle or plunkr illustrating your problem? – jbll Mar 25 '14 at 06:57
  • @RichardBender Couldn't you use a single tip that moves around the chart rather than creating new tips ? – Union find Feb 10 '15 at 00:12
  • @jbll, This works but this is a hack. You destroy the directive to manually compile the element. If you do not remove the directive, then the compile will not work. Please see david004's answer. – James Jan 29 '17 at 21:41
17

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)
            }
        }
    };
}]);
dave walker
  • 3,058
  • 1
  • 24
  • 30
2

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.

btm1
  • 3,866
  • 2
  • 23
  • 26
2

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);
        }
    };
}]);
NTyler
  • 1,397
  • 1
  • 12
  • 20
0

@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 );
  } );
}
Philip Bulley
  • 9,014
  • 3
  • 33
  • 46