Interactive plots in Jupyter (IPython) notebook wi

2019-04-04 00:02发布

问题:

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.

回答1:

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


回答2:

tl;dr - Here's a link to the gist showing update-on-drag.


To do this you need to know:

  • How to interact with the IPython kernel from Jupyter's Javascript frontend. Right now that's via Jupyter.Kernel.execute (current source code ).
  • Enough d3.js to be comfortable. (Like with screen to plot coordinate conversion.)
  • The d3-via-Python library of your choice. mpld3 for this example.

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.

Python

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

Javascript

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)}