Интерактивные графики в ноутбуке Jupyter (IPython) с перетаскиваемыми точками, которые вызывают код Python при перетаскивании

Я хотел бы сделать некоторые интерактивные сюжеты в ноутбуке Jupyter, в котором некоторые точки в сюжете могут быть перетаскиваны пользователем. Затем местоположения этих точек должны использоваться в качестве входных данных для функции Python (в блокноте), которая обновляет график.

Что-то вроде этого было выполнено здесь:

http://nbviewer.ipython.org/github/maojrs/ipynotebooks/blob/master/interactive_test.ipynb

но обратные вызовы относятся к функциям Javascript. В некоторых случаях код, который обновляет сюжет, должен быть чрезвычайно сложным и займет очень много времени, чтобы переписать в Javascript. Я желаю назначить перетаскиваемые точки в Javascript, если это необходимо, но можно ли перезвонить на Python для обновления графика?

Мне интересно, могут ли такие инструменты, как Bokeh или Plotly, предоставить эту функциональность.

Ответы

Ответ 1

Вы пробовали bqplot? Scatter имеет параметр enable_move, который, когда вы устанавливаете на True, позволяет перетаскивать точки. Кроме того, при перетаскивании вы можете наблюдать изменение значения x или y Scatter или Label и запускать через него функцию python, которая, в свою очередь, генерирует новый график. Они делают это в ноутбуке Introduction.

Код ноутбука Jupyter:

# Let 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 title
plt.figure(title='My Second Chart')
# Let assign the scatter plot to a variable
scatter_plot = plt.scatter(x_data, y_data)

# Let 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 - Здесь ссылка на суть, показывающая обновление при перетаскивании.


Для этого вам нужно знать:

  • Как взаимодействовать с ядром IPython от Jupyter Javascript внешний интерфейс. Прямо сейчас, через Jupyter.Kernel.execute (current исходный код).
  • Достаточно d3.js, чтобы быть комфортным. (Как с экраном для преобразования координат).
  • Библиотека d3-via-Python по вашему выбору. mpld3 для этого примера.

mpld3 имеет свой собственный плагин для перетаскиваемых точек и возможность custom Плагин mpld3. Но сейчас нет возможности перерисовывать график при обновлении данных; разработчики говорят, что лучший способ сделать это - удалить и перерисовать весь сюжет при обновлении, или же действительно погрузиться в javascript.

Ipywidgets, как вы сказали (и насколько я могу судить), способ связать элементы HTML input с диаграммами Jupyter при использовании ядра IPython и, следовательно, не совсем то, что вы хотите. Но в тысячу раз легче, чем я предлагаю. Ipywidgets github repo README ссылки на правильный IPython ноутбук для начала с в их примерном пакете.


Лучшее сообщение в блоге о прямом взаимодействии ноутбука Jupyter с ядром IPython от Jake Vanderplas в 2013 году. Это для IPython <= 2.0 и комментаторов как недавно несколько месяцев назад (август 2015 г.) были опубликованы обновления для IPython 2 и IPython 3, но код не работал с моим ноутбуком Jupyter 4. Проблема заключается в том, что javascript API для ядра Jupyter находится в движении.

Я обновил пример перетаскивания mpld3 и пример Jake Vanderplas в строке (ссылка находится в верхней части этого ответа), чтобы дать как можно меньше пример, насколько это возможно, так как это уже давно, но нижеприведенные фрагменты пытаются передать идею более кратко.

Python

Обратный вызов Python может иметь столько аргументов, сколько требуется, или даже быть необработанным кодом. Ядро запускает его через оператор eval и возвращает последнее возвращаемое значение. Результат, независимо от того, какой тип он будет, будет передан как строка (text/plain) в обратный вызов javascript.

def python_callback(arg):
    """The entire expression is evaluated like eval(string)."""
    return arg + 42

Javascript

Обратный вызов Javascript должен принимать один аргумент, который является Javascript Object, который подчиняется структуре описанной здесь.

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.
  //
}

Вызвать ядро ​​IPython из Jupyter с помощью функции Jupyter.notebook.kernel.execute. Содержимое, отправленное на Ядро описано здесь.

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 внутри плагина mpld3

Измените плагин библиотеки mpld3, чтобы добавить уникальный класс в HTML-элементы, которые необходимо обновить, чтобы мы могли найти их снова в будущее.

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