Simple IPython Widgets + D3 Example

By Jake Vanderplas, May 2014

This is the result of an afternoon playing around with the IPython widgets framework, and learning how to do two-directional callbacks between the Python kernel and the Javascript frontend.

Note that this requires a Python kernel to run; that is, if you're looking at this on nbviewer it won't work!

Here's the summary:

  • We create a CircleView widget in Python, and a corresponding CircleView object in Javascript.
  • When the mouse hovers over the circle, we fire off a series of events
    • The javascript event sends a message to the Python kernel, saying a mouseover has happened.
    • This triggers the generation of some random numbers in Python
    • These numbers are then sent back to the JS frontend, updating the diagram

Below is the code that makes it all happen.

Python-side Widget Object

In [1]:
from IPython.html import widgets
from IPython.utils.traitlets import Unicode


class CircleView(widgets.DOMWidget):
    _view_name = Unicode('CircleView', sync=True)

    def __init__(self, *pargs, **kwargs):
        widgets.DOMWidget.__init__(self, *pargs, **kwargs)
        self._handlers = widgets.CallbackDispatcher()
        self.on_msg(self._handle_my_msg)

    def _ipython_display_(self, *pargs, **kwargs):
        widgets.DOMWidget._ipython_display_(self, *pargs, **kwargs)

    def _handle_my_msg(self, _, content):
        """handle a message from the frontent"""
        if content.get('event', '') == 'mouseover':
            self._handlers(self)

    def on_mouseover(self, callback):
        """Register a callback at mouseover"""
        self._handlers.register_callback(callback)

Javascript side of the widget object

In [2]:
%%javascript

require(["//cdnjs.cloudflare.com/ajax/libs/d3/3.4.1/d3.min.js",
         "widgets/js/widget"], function(d3, WidgetManager){

  var CircleView = IPython.DOMWidgetView.extend({

    render: function(){
            this.guid = 'circle' + IPython.utils.uuid();
            this.setElement($('<div />', {id: this.guid}));
            
            this.model.on('msg:custom', this.on_msg, this);
            this.has_drawn = false;

            // Wait for element to be added to the DOM
            var that = this;
            setTimeout(function() {
                that.update();
            }, 0);
    },

    update: function(){
        var that = this;

        if (!this.has_drawn) {
           this.has_drawn = true;

           this.svg = d3.select("#" + this.guid).append("svg")
               .attr("width", 200)
               .attr("height", 200);

           this.circle = this.svg.append("circle")
                    .attr("cx", 100)
                    .attr("cy", 100)
                    .attr("r", 20)
                    .style("fill", "red")
                    .style("fill-opacity", 0.5)
                    .on("mouseenter", function(){that.send({event:'mouseover'})});
       }
       return CircleView.__super__.update.apply(this);
    },

    on_msg: function(attrs){
        this.circle.transition().attr(attrs).style(attrs);
    }
  });
  WidgetManager.register_widget_view('CircleView', CircleView);
})

Creating the object and adding a callback

In [5]:
from IPython.display import display
from random import randint

colors = ['blue', 'green', 'orange', 'black', 'magenta', 'red']

def update_circle(view):
    view.send({"cx": randint(30, 170),
               "cy": randint(30, 170),
               "r": randint(10, 30),
               "fill": colors[randint(0, 5)]})

circle = CircleView()
circle.on_mouseover(update_circle)

print("Try to catch the circle!")
display(circle)
Try to catch the circle!