notebookJS: JavaScript и D3 в Jupyter Notebook

Вы когда-нибудь задумывались о том, как здорово было бы использовать визуализацию данных с помощью D3 или React в Jupyter Notebook? Много раз, скорее всего. Jupyter Notebook и Google Colab Notebook идеально подходят для экспериментов с визуализацией данных: в одном месте удобно собраны логика обработки данных, препроцессинг, иллюстрации и документация.

Однако для добавления JavaScript в Notebook на Python потребуется громоздкий и повторяющийся код шаблонов. Что еще хуже, разные Notebook-среды имеют несовместимые API для передачи сообщений между Python и JavaScript. Например, если разработчик хочет, чтобы его библиотека работала как в Jupyter, так и в Colab, то ему придется использовать два разных формата связи  —  это неудобно.

Python-библиотека notebookJSбыла создана, чтобы решить эти проблемы. Теперь можно использовать JavaScript в аналитике данных на Python с помощью одной строчки кода!

Повторное использование D3 scatterplot в Jupyter Notebook

Предпосылки

Написание визуализации с помощью любимого веб-стека (например, D3 и React) добавляет программированию гибкости. Однако в таком случае тестирование визуализации на разных наборах данных может стать проблемой. По моему опыту, мне приходилось либо жестко кодировать нужный датасет, либо писать собственный сервер для управления данными; ни один из вариантов не подходит для множественных экспериментов с разными наборами данных, препроцессингом или диаграммами.

// Жесткое кодирование датасета
d3.csv("data.csv", function(error, data) {
  // Рисуем супер крутую диаграмму!
}

Экспериментировать в Python Notebooks  —  очень удобно, поскольку манипуляции с данными выполняются оперативно, а блокнот автоматически отслеживает все этапы вычислений. Обычно возможностей библиотек Python, таких как Matplotlib и Altair, полностью хватает для построения графика. Но бывают случаи, когда необходимой визуализации данных невозможно добиться при помощи готовых решений. Например, при работе с ML-конвейерами на базе Auto-Sklearn визуализацию придется написать с нуля. 

Чтобы интегрировать пользовательские визуализации в Jupyter Notebook, придется написать много шаблонного кода: непосредственно для запуска визуализации в среде, а также для организации обмена данными между Python и JavaScript.

Поскольку красивая и детальная визуализация данных в Jupyter Notebook пригодится многим людям, было решено создать специальную библиотеку. Так и был разработан notebookJS, который предоставляет следующие возможности. 

  • Позволяет избежать многократного написания одинакового шаблонного кода для запуска визуализации в Jupyter Notebook.
  • Автоматически подгружает библиотеки JavaScript из сети.
  • Поддерживает разделение JavaScript и CSS кода на несколько отдельных файлов.

notebookJS

Для начала нужно скачать саму библиотеку:

pip install notebookjs

API notebookJS крайне прост в использовании. На самом-то деле обо всем позаботится лишь одна функция: метод execute_js выполнит JavaScript-функцию и самостоятельно настроит инфраструктуру для двунаправленной связи между Python и JavaScript с помощью обратных вызовов.

execute_js(
  library_list,
  main_function,
  data_dict={},
  callbacks={},
  css_list=[],
)

Рассмотрим параметры функции execute_js подробнее:

  • library_list—  это список из строк, содержащий ссылку на JavaScript-библиотеку, JavaScript-код и JavaScript-пакет.
  • main_function—  это строка, содержащая идентификатор главной функции для её вызова. Главная функция вызывается с двумя параметрами: <div_id> (#my_div) и <data_dict>.
  • data_dict —  это словарь, содержащий те данные, которые предполагается использовать в качестве входных для главной функции.
  • callbacks—  это словарь, содержащий пары ключ-значение в формате {<callback_str_id> : <python_function>}. Собственно, данный словарь позволяет JavaScript-библиотеке использовать обратные вызовы для обмена данными с Python.
  • css_list —  это список из строк, содержащий ссылку на CSS-файл или же сами стили CSS.

При вызове функции execute_js также произойдет вызов указанной main_function со следующей сигнатурой:

function main_function(div_id, data_dict)

В качестве простого примера рассмотрим использование JavaScript-библиотеки для визуализации данных D3. Нарисуем круг в ячейке вывода:

# Установка ссылки на библиотеку D3
d3_lib_url = "https://d3js.org/d3.v3.min.js"

# Определение JS-функции при помощи Python-строки
js_string = “””
function draw_circle(div_id, data){
  d3.select(div_id)
    .append("div")
    .style("width", "50px")
    .style("height", "50px")
    .style("background-color", data.color)
    .style("border-radius", "50px")
}
“””

# Исполнение кода с использованием notebookjs
from notebookjs import execute_js
execute_js([d3_lib_url, draw_circle_lib], "draw_circle", {"color": "#4682B4"})
Результат

Загрузка локальных библиотек

Как мы могли удостовериться, запуск JavaScript-функции в среде Python Notebook осуществляется всего лишь одной строчкой кода (исключая настройку JavaScript-библиотеки). Стоит отметить, что JavaScript-код можно сохранить в отдельный файл, в последствии загружаемый через Python. Данные для визуализации при необходимости также загружаются из отдельного файла через Python, а передать их в JavaScript позволяет структура словаря (с внутренней конвертацией в JSON-формат).

Например, для многократного использования радиальной гистограммы достаточно всего лишь изменить Python-код загрузки набора данных и запустить notebookJS:

# Указание ссылки на библиотеку D3
d3_lib_url = "https://d3js.org/d3.v3.min.js"

# Загрузка локальных библиотек 
with open("radial_bar.css", "r") as f:
    radial_bar_css = f.read()
    
with open ("radial_bar_lib.js", "r") as f:
    radial_bar_lib = f.read()

# Загрузка данных
import pandas as pd
energy = pd.read_csv("energy.csv").to_dict(orient="records")

# Построение диаграммы с помощью notebookJS
from notebookjs import execute_js
execute_js(library_list=[d3_lib_url, radial_bar_lib], main_function="radial_bar",
             data_dict=energy, css_list=[radial_bar_css])
Результат: анимированная радиальная гистограмма

Обратные вызовы Python

Одно из лучших преимуществ notebookJs  —  это возможность настройки обратных вызовов Python. Во время активной работы над проектами в области науки о данных некоторые задачи удобнее и быстрее выполнить с помощью JavaScript (как пример  —  создание интерфейса и взаимодействий с пользователем). Но вот другие задачи оптимальнее выполняются с использованием Python (например, сложные вычисления и машинное обучение). Библиотека notebookJs как раз позволит нам применить на практике всё лучшее из обоих миров.

Рассмотрим элементарный пример: необходимо создать анимацию, выводящую на экран словосочетание “Hello World” на разных языках. Фразы хранятся в Python, а код интерфейса обрабатывается с помощью JavaScript.

JavaScript-функция отображения каждую секунду запрашивает у Python новую фразу “Hello World” с помощью идентификатора “get_hello”:

helloworld_js = """
function helloworld(div_id, data){
    comm = new CommAPI("get_hello", (ret) => {
      document.querySelector(div_id).textContent = ret.text;
    });
    setInterval(() => {comm.call({})}, 1000);
    comm.call({});
}
"""

Python постоянно ожидает ежесекундного исполнения JavaScript-функции с идентификатором get_hello и незамедлительно отвечает:

import random
def hello_world_random(data):
  hello_world_languages = [
      "Ola Mundo", # Португальский
      "Hello World", # Английский
      "Hola Mundo", # Испанский
      "Geiá sou Kósme", # Греческий
      "Kon'nichiwa sekai", # Японский
      "Hallo Welt", # Немецкий
      "namaste duniya" # Хинди
  ]
  i = random.randint(0, len(hello_world_languages)-1)
  return {'text': hello_world_languages[i]}

Функция обратного вызова “соединена” с визуализацией благодаря специальному параметру обратного вызова:

from notebookjs import execute_js
execute_js(helloworld_js, "helloworld", callbacks={"get_hello": hello_world_random})
Результат: ежесекундное изменение фразы “Hello World”

Ограничения

Пока что notebookJS не поддерживает библиотеки JavaScript ES6. Исходя из этого, в примерах используется библиотека D3 V3.

Если вам необходим JavaScript ES6, рекомендуется использовать инструмент сборки, такой как Webpack + Babel. С его помощью вы сможете скомпилировать весь JavaScript-код в один файл, чтобы преобразовать JavaScript ES6 в старый JavaScript.

Так как проектам свойственно расширяться, вышеописанный способ лучше всего подходит для управления несколькими библиотеками, а также для создания хорошо оптимизируемого кода. Узнать больше о настройке библиотеки Webpack можно в соответствующем разделе репозитория notebookJS на GitHub.

Итоги

notebookJS  —  это новый инструмент. Создатели библиотеки активно собирают отзывы о том, какие возможности программы им следует поддерживать. Дайте им знать, если у вас есть какие-либо вопросы или вы желаете сотрудничать!

В репозитории notebookJS на GitHub можно найти куда больше примеров. Живая демонстрация работы проекта доступна в соответствующем Colab Notebook.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Jorge Piazentin Ono: Introducing notebookJS: seamless integration between Python and JavaScript in Computational Notebooks

Предыдущая статья7 факторов, которые помогут улучшить UX-дизайн
Следующая статьяКонвейер BitBucket CI/CD для синхронизации веток с GitHub