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:
CircleView
widget in Python, and a corresponding CircleView
object in Javascript.Below is the code that makes it all happen.
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
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);
})
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!