I'd like to make some interactive plots in the Jupyter notebook, in which certain points in the plot can be dragged by the user. The locations of those points should then be used as input to a Python function (in the notebook) that updates the plot.
Something like this has been accomplished here:
http://nbviewer.ipython.org/github/maojrs/ipynotebooks/blob/master/interactive_test.ipynb
but the callbacks are to Javascript functions. In some cases, the code that updates the plot needs to be extremely complex and would take a very long time to rewrite in Javascript. I'm willing to designate the draggable points in Javascript if necessary, but is it possible to call back to Python for updating the plot?
I'm wondering if tools like Bokeh or Plotly could provide this functionality.
Jupyter Notebook has support for many kinds of interactive outputs, including the ipywidgets ecosystem as well as many interactive visualization libraries.
The %matplotlib magic command sets up your Jupyter Notebook for displaying plots with Matplotlib. The standard Matplotlib graphics backend is used by default, and your plots will be displayed in a separate window.
Have you tried bqplot? The Scatter
has an enable_move
parameter, that when you set to True
they allow points to be dragged. Furthermore, when you drag you can observe a change in the x
or y
value of the Scatter
or Label
and trigger a python function through that, which in turn generates a new plot. They do this in the Introduction notebook.
Jupyter notebook code:
# Let's begin by importing some libraries we'll need
import numpy as np
from __future__ import print_function # So that this notebook becomes both Python 2 and Python 3 compatible
# And creating some random data
size = 10
np.random.seed(0)
x_data = np.arange(size)
y_data = np.cumsum(np.random.randn(size) * 100.0)
from bqplot import pyplot as plt
# Creating a new Figure and setting it's title
plt.figure(title='My Second Chart')
# Let's assign the scatter plot to a variable
scatter_plot = plt.scatter(x_data, y_data)
# Let's show the plot
plt.show()
# then enable modification and attach a callback function:
def foo(change):
print('This is a trait change. Foo was called by the fact that we moved the Scatter')
print('In fact, the Scatter plot sent us all the new data: ')
print('To access the data, try modifying the function and printing the data variable')
global pdata
pdata = [scatter_plot.x,scatter_plot.y]
# First, we hook up our function `foo` to the colors attribute (or Trait) of the scatter plot
scatter_plot.observe(foo, ['y','x'])
scatter_plot.enable_move = True
tl;dr - Here's a link to the gist showing update-on-drag.
To do this you need to know:
Jupyter.Kernel.execute
(current
source code ).mpld3 has its own plugin for draggable points and capability for a custom mpld3 plugin. But right now there is no feature to redraw the plot on update of data; the maintainers say right now the best way to do this is to delete and redraw the whole plot on update, or else really dive into the javascript.
Ipywidgets is, like you said (and as far as I can tell), a way to link up HTML input
elements to Jupyter notebook plots when using the IPython kernel, and so not quite what you want. But a thousand times easier than what I'm proposing. The ipywidgets github repo's README links to the correct IPython notebook to start with in their example suite.
The best blog post about direct Jupyter notebook interaction with the IPython kernel is from Jake Vanderplas in 2013. It's for IPython<=2.0 and commenters as recent as a few months ago (August 2015) posted updates for IPython 2 and IPython 3 but the code did not work with my Jupyter 4 notebook. The problem seems to be that the javascript API for the Jupyter kernel is in flux.
I updated the mpld3 dragging example and Jake Vanderplas's example in a gist (the link is at the top of this reply) to give as short an example as possible since this is already long, but the snippets below try to communicate the idea more succinctly.
The Python callback can have as many arguments as desired, or even be raw code. The kernel will run it through an eval
statement and send back the last return value. The output, no matter what type it is, will be passed as a string (text/plain
) to the javascript callback.
def python_callback(arg):
"""The entire expression is evaluated like eval(string)."""
return arg + 42
The Javascript callback should take one argument, which is a Javascript
Object
that obeys the structure documented here.
javascriptCallback = function(out) {
// Error checking omitted for brevity.
output = out.content.user_expressions.out1;
res = output.data["text/plain"];
newValue = JSON.parse(res); // If necessary
//
// Use newValue to do something now.
//
}
Call the IPython kernel from Jupyter using the function Jupyter.notebook.kernel.execute
. The content sent to the
Kernel is documented here.
var kernel = Jupyter.notebook.kernel;
var callbacks = {shell: {reply: javascriptCallback }};
kernel.execute(
"print('only the success/fail status of this code is reported')",
callbacks,
{user_expressions:
{out1: "python_callback(" + 10 + ")"} // function call as a string
}
);
Javscript inside the mpld3 plugin
Modify the mpld3 library's plugin to add a unique class to the HTML elements to be updated, so that we can find them again in the future.
import matplotlib as mpl
import mpld3
class DragPlugin(mpld3.plugins.PluginBase):
JAVASCRIPT = r"""
// Beginning content unchanged, and removed for brevity.
DragPlugin.prototype.draw = function(){
var obj = mpld3.get_element(this.props.id);
var drag = d3.behavior.drag()
.origin(function(d) { return {x:obj.ax.x(d[0]),
y:obj.ax.y(d[1])}; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
// Additional content unchanged, and removed for brevity
obj.elements()
.data(obj.offsets)
.style("cursor", "default")
.attr("name", "redrawable") // DIFFERENT
.call(drag);
// Also modify the 'dragstarted' function to store
// the starting position, and the 'dragended' function
// to initiate the exchange with the IPython kernel
// that will update the plot.
};
"""
def __init__(self, points):
if isinstance(points, mpl.lines.Line2D):
suffix = "pts"
else:
suffix = None
self.dict_ = {"type": "drag",
"id": mpld3.utils.get_id(points, suffix)}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With