These are some example widgets, built with the Comm API.
First, is a simple Range, hooking up change events on traits to a slider in Javascript.
import sys, time
from IPython.utils.traitlets import HasTraits, Integer, Unicode, Bool, Instance, Float
from IPython.display import display
from IPython.kernel.comm import Comm
class RangeWidget(HasTraits):
"""A Widget for a value on a Range"""
min = Float(0)
max = Float(100)
value = Float(50)
step = Float(1)
_keys = ['min', 'max', 'value', 'step']
_handling = Bool(False)
comm = Instance(Comm)
def _comm_default(self):
return Comm(target='range', _open_data=self.data)
def _validate_and_update(self, name, old, new):
if self.value > self.max:
raise ValueError("value %d exceeds max %d" % (self.value, self.max))
if new < self.min:
raise ValueError("value %d below min %d" % (self.value, self.min))
if not self._handling:
self.update()
def __init__(self, **kwargs):
super(RangeWidget, self).__init__(**kwargs)
# register on_trait_change after init
# otherwise default values can't be specified
self.on_trait_change(self._validate_and_update, self._keys)
# register handler on message
self.comm.on_msg(self.handle_msg)
@property
def data(self):
return dict((key, getattr(self, key)) for key in self._keys)
def update(self):
"""send my state to my js peer"""
self.comm.send(self.data)
def __repr__(self):
return "<Range (%.1f) %.1f:%.1f:%.1f>" % (self.value, self.min, self.max, self.step)
def handle_msg(self, data):
self._handling = True
try:
for key in self._keys:
if key in data:
setattr(self, key, data[key])
finally:
self._handling = False
sys.stdout.write('\r%s' % self)
sys.stdout.flush()
def display(self):
display(self)
self.update()
def _repr_html_(self):
return """<div class="%s"></div>""" % self.comm.comm_id
The Javascript side
%%javascript
var RangeWidget = function (comm) {
this.comm = comm;
this.keys = ['min', 'max', 'step'];
$([this.comm]).on("comm_open", $.proxy(this.handle_open, this));
$([this.comm]).on("comm_msg", $.proxy(this.handler, this));
};
RangeWidget.prototype.get_div = function () {
var div = $("div." + this.comm.comm_id);
if (div.length === 0) {
return null;
}
if (div.find("input").length === 0) {
this.create_range(div);
}
return div;
};
RangeWidget.prototype.create_range = function (thediv) {
var range = $('<input></input>').attr('type', 'range');
var data = this.data;
for (var i=0; i < this.keys.length; i++) {
var key = this.keys[i];
range.attr(key, data[key]);
}
range.val(data.value);
thediv.append(
$("<span/>").addClass("min").text(data.min)
).append(
range
).append(
$("<span/>").addClass("max").text(data.max)
).append(
$("<br/>")
).append(
$("<span/>").addClass("value").text(data.value)
);
range.on("change", $.proxy(this.value_changed, this));
}
RangeWidget.prototype.handle_open = function (evt, data) {
console.log('range open', this, evt, data);
this.data = data;
this.get_div();
};
RangeWidget.prototype.value_changed = function (evt) {
var now = new Date();
if (now - this.last_changed < 100) {
// only update every 0.1s, max
// return;
}
var div = this.get_div();
var range = div.find("input");
var data = {};
for (var i=0; i < this.keys.length; i++) {
var key = this.keys[i];
data[key] = parseFloat(range.attr(key));
}
data.value = parseFloat(range.val());
div.find("span.min").text(data.min);
div.find("span.max").text(data.max);
div.find("span.value").text(data.value);
this.comm.send(data);
this.last_changed = now;
};
RangeWidget.prototype.handler = function (evt, data) {
console.log('range update', evt, data);
this.data = data;
var div = this.get_div();
if (!div) {
return;
}
var range = div.find("input");
for (var i=0; i < this.keys.length; i++) {
var key = this.keys[i];
range.attr(key, data[key]);
}
div.find("span.min").text(data.min);
div.find("span.max").text(data.max);
div.find("span.value").text(data.value);
range.val(data.value);
};
var new_range = function (comm) {
var r = new RangeWidget(comm);
};
IPython.comm_manager.register_target('range', function (comm) { var r = new RangeWidget(comm);});
Create the Python object, and register it with the WidgetManager. Registration triggers creation of the javascript-side counterpart.
r = RangeWidget()
r.display()
r.value = 5
print r
<Range (5.0) 0.0:100.0:1.0> <Range (45.0) 0.0:100.0:1.0>
import math, time
import numpy as np
x = np.linspace(0,10,1000)
y = 50 + (np.sin(x) * 50)
for n in y:
r.value = (n)
time.sleep(0.005)
from IPython.utils.traitlets import List
class FlotWidget(HasTraits):
lines = List()
comm = Instance(Comm, kw=dict(target='flotplot'))
def show(self):
self.comm.send(dict(lines=self.lines))
def plot(self, x, y):
self.lines.append(list(zip(x,y)))
self.show()
def clear(self):
self.lines = []
self.show()
def _repr_html_(self):
return '<div id="%s" class="flotwidget"></div>' % self.comm.comm_id
%%javascript
$.getScript('//cdnjs.cloudflare.com/ajax/libs/flot/0.8.1/jquery.flot.min.js');
$("<style type='text/css'> .flotwidget{ width: 100%; height: 300px;}</style>").appendTo("head");
var FlotWidget = function (comm) {
this.comm = comm;
$([this.comm]).on("comm_msg", $.proxy(this.handler, this));
};
FlotWidget.prototype.handler = function (evt, data) {
// console.log('flot', data);
$.plot($('#' + this.comm.comm_id), data.lines);
}
IPython.comm_manager.register_target('flotplot', function (comm) { var f = new FlotWidget(comm); console.log(f)});
flot = FlotWidget()
flot
flot.clear()
x = np.linspace(0,5,200)
y = np.sin(x)
flot.plot(x,np.sin(x))
flot.plot(x,np.cos(x))
flot.plot(x, np.sin(2*x))
from IPython.utils.traitlets import HasTraits, Instance, Any
class SineOmegaT(HasTraits):
omega = Instance(RangeWidget, kw=dict(min=0, max=10, step=0.1, value=1), allow_none=False)
flot = Instance(FlotWidget, args=())
# can't use traitlets with numpy arrays
t = None
y = None
_in_omega_changed = False
def _omega_value_changed(self, name, old, new):
self.y = np.sin(new * self.t)
self.show()
def __init__(self):
super(SineOmegaT, self).__init__()
self.omega.on_trait_change(self._omega_value_changed, 'value')
self.t = np.linspace(0,10,200)
self.y = np.sin(self.omega.value * self.t)
# add HTML to the dom
display(self)
# ensure omega slider is drawn
self.omega.update()
self.last_draw = 0
self.show()
def show(self):
now = time.time()
if False and (now - self.last_draw) < 0.1:
return
self.last_draw = now
self.flot.lines = []
self.flot.plot(self.t, self.y)
def _repr_html_(self):
return u"$$sin(\omega t)$$<br>%s<br>$\omega$: %s" % (self.flot._repr_html_(), self.omega._repr_html_())
sino = SineOmegaT()
<Range (1.6) 0.0:10.0:0.1>
sino.omega.max = 30
sino.omega.step = 1
<Range (4.0) 0.0:30.0:1.0>
sino.t = np.linspace(0,100,10)
sino.show()
<Range (20.0) 0.0:30.0:1.0>
This time, we expose an RPC object, using messages of the following form:
{
'method' : 'the_method_name',
'args' : [list, of, args],
'kwargs' : { 'kwarg' : 'dict' },
}
from IPython.kernel.comm import Comm
from IPython.core.getipython import get_ipython
class RPC(object):
"""An object whose methods are exposed via RPC"""
def __init__(self, comm):
self.comm = comm
self.comm.on_msg(self.handler)
def add(self, a, b):
return a + b
def mul(self, x, y):
return x * y
def get_execution_count(self):
return get_ipython().execution_count
def handler(self, data):
"""this implements RPC"""
method = getattr(self, data['method'])
args = data.get('args', ())
kwargs = data.get('kwargs', {})
result = method(*args, **kwargs)
self.comm.send(dict(result=result, method=data['method']))
get_ipython().comm_manager.register_target('rpc', RPC)
The RPCProxy lives in Javascript, and creates the Kernel-side counterpart. Results are posted to the notification area in the upper right.
%%javascript
var RPCProxy = function () {
console.log(this);
this.comm = new IPython.Comm(IPython.utils.uuid());
this.comm.target = 'rpc';
$([this.comm]).on("comm_msg", $.proxy(this.handler, this));
IPython.comm_manager.register_comm(this.comm);
this.comm.open();
try {
this.notification = IPython.notification_area.get_widget('rpc');
} catch (e) {
this.notification = IPython.notification_area.new_notification_widget('rpc');
}
};
RPCProxy.prototype.handler = function (evt, data) {
console.log('rpc', data);
this.notification.set_message(JSON.stringify(data), 5000);
}
RPCProxy.prototype.add = function (a,b) {
this.comm.send({method : 'add', args : [a,b]});
}
RPCProxy.prototype.mul = function (a,b) {
this.comm.send({method : 'mul', args : [a,b]});
}
RPCProxy.prototype.get_ec = function () {
this.comm.send({method : 'get_execution_count'});
}
var rpc = new RPCProxy();
rpc.add(5,3);
setTimeout(function () { rpc.get_ec(); }, 2000);
setTimeout(function () { rpc.mul(6,6); }, 4000);