Interactive plots in Jupyter (IPython) notebook with draggable points that call Python code when dragged

后端 未结 2 1430
太阳男子
太阳男子 2021-02-19 00:52

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 u

2条回答
  •  情深已故
    2021-02-19 01:32

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

提交回复
热议问题