В этой публикации подробно рассказывается о моих первых детских шагах по объединению серверной экосистемы и выразительности Python, интуитивности Jupyter и интерактивности на стороне клиента Javascript для создания «идеального приложения для всего».

Позвольте мне сначала немного рассказать о конкретной мотивации того, что я пытаюсь сделать. На работе я довольно часто использую MapboxGL для визуализации больших данных на карте. Это отличная библиотека Javascript для отображения клиентских карт с ускорением на GPU с наложенными данными.

Основная цель нашего веб-приложения - сделать науку о геопространственных данных доступной для обычных людей. Хотя наши целевые аналитики могут быть экспертами по Excel, моя личная цель - заставить их использовать Jupyter Notebook, и с этой целью я стремлюсь интегрировать MapboxGL в Jupyter для улучшения качества интерактивных карт: проектировать данные с помощью Python / Pandas, а также легко отображать и анимировать их.

Райан Бауманн и его участники создали библиотеку mapboxgl-jupyter, но она ограничена тем, что карта, отображаемая в Jupyter, является статической, это просто ячейка, которая отображает iframe со всеми данными для карты прямо в источнике iframe! Мое видение состоит в том, что по мере того, как пользователь изменяет данные во фрейме данных Pandas, карта должна обновляться реактивно, обеспечивая надлежащий исследовательский анализ и визуализацию тенденций и движения.

Вот как выглядит вывод библиотеки: iframe со встроенными данными:

<iframe id="map" ,="" srcdoc="<!DOCTYPE html>
<html>
<head>
<title>mapboxgl-jupyter viz</title>
<meta charset='UTF-8' />
<meta name='viewport'
      content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<script type='text/javascript'
        src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.41.0/mapbox-gl.js'></script>
...
map.on('load', function() {
    
    map.addSource('data', {
        'type': 'geojson',
        'data': {'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'properties': {'Avg Medicare Payments': 7678.21, 'Avg Covered Charges': 35247.03}, 'geometry': {'type': 'Point', 'coordinates': [-85.3629, 31.2162]}}, {'type': 'Feature', 'properties': {'Avg Medicare Payments': 5793.63, 'Avg Covered Charges': 16451.09}, 'geometry': {'type': 'Point', 'coordinates': [-88.1428, 32.453]}}, {'type': 'Feature', 'properties': {'Avg Medicare Payments': 7145.96, 'Avg Covered Charges': 36942.36}, 'geometry': {'type': 'Point', 'coordinates': [-87.6829, 34.7941]}},
[... etc!]

Однако то, что я здесь пытаюсь сделать, не ограничивается работой с MapboxGL. Это было бы в равной степени применимо для создания достойного плагина d3.js для Jupyter (вы можете представить себе, что matplotlib когда-нибудь будет вытеснен!) Или побочного проекта, которым я собираюсь заняться следующим: синтез WebAudio на базе за счет обработки чисел Python с помощью дружественного и интуитивно понятного интерфейса Jupyter (точно так же, если вы знаете какие-либо звуковые материалы, можете ли вы представить, что это вытесняет мощный, но сложный SuperCollider!)

Внутренности Jupyter

Мне нравится Jupyter, но, исследуя, как решить эту проблему, я с грустью обнаружил, что он использует JQuery и Backbone.js (правда!)

Мой первый подход заключался в создании расширения для записной книжки, но оказалось, что они больше предназначены для добавления в пользовательский интерфейс записной книжки. Я не мог найти простой способ связи с ядром Python, которое на самом деле выполнялось записной книжкой.

Именно тогда я наткнулся на виджеты. Во-первых, вам нужно включить расширение блокнота виджетов:

jupyter nbextension enable — py widgetsnbextension

Если расширение еще не установлено, conda install widgetsnbextension следует исправить его.

Переменные Python, запускающие события Javascript

Это стало возможным благодаря чертам характера, с которыми я раньше не сталкивался. Traitlets - это библиотека для IPython, которая создает новые типы Python (на основе знакомых, таких как list, string, dict и т. Д.), Где переменные этих типов имеют дополнительные возможности: в частности, строгую проверку типов и, что наиболее важно для нашей цели: уведомления, обратные вызовы, которые происходят при изменении значения объекта. Трейтлеты основаны на библиотеке черт, которую я мог бы использовать, если бы мне когда-либо понадобилось создать настольное приложение с графическим интерфейсом пользователя на Python.

Сначала я начал с канонического примера Hello World для виджетов Jupyter. Но у него было два ограничения; во-первых, мне нужно использовать JSON для связи с картой (т. е. сопоставление между объектом Javascript и dict Python), а во-вторых, мне нужно, чтобы это был готовый к использованию модуль Python из коробки, магия Javascript победила » t сократить это.

(Мне никогда раньше не нужно было использовать магию Javascript в Jupyter, но оказалось, что это всего лишь код, выполняемый точно так, как если бы вы открыли инструменты разработчика Chrome и вставили его в консоль JS).

Встраивание Javascript в Python

На самом деле это не так уж сложно. Можно добавить на страницу произвольный HTML-код, включая Javascript, с помощью модуля IPython.core.display. (Вы даже можете делать такие непослушные вещи, как window.open).

from IPython.core.display import HTML
HTML("<script>console.log('Hello, world!');</script>")

Не совсем Python как таковой, но он работает с любым ядром IPython / Jupyter. Более того, вы можете отказаться от тега script, импортировав вместо него Javascript:

from IPython.core.display import Javascript
Javascript("alert('Hello, world!')")

Превращаем его в отдельный модуль

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

import ipywidgets as widgets
class MapboxWidget(widgets.DOMWidget):
    from traitlets import Dict, Unicode
    _view_name = Unicode('MapboxGLView').tag(sync=True)
    _view_module = Unicode('mapboxglModule').tag(sync=True)
    _view_module_version = Unicode('0.1.0').tag(sync=True)
    value = Dict({"my_key": "Hello"}).tag(sync=True)
    def create(self):
        from IPython.core.display import Javascript
        return Javascript("""
            require.undef('mapboxglModule');
            define('mapboxglModule', ["@jupyter-widgets/base"], widgets => {
            let MapboxGLView = widgets.DOMWidgetView.extend({
                    defaults: _.extend({}, widgets.DOMWidgetModel.prototype.defaults, { value: '' }),
                    render: function() {
                        this.value_changed();
                        this.model.on('change:value', this.value_changed, this);
                    },
            value_changed: function() {
                        this.el.textContent = this.model.get('value').my_key;
                    },
                });
            return {
                    MapboxGLView,
                };
            });
        """)

Там есть немного шаблонов, и JS не очень аккуратный, но цель, надеюсь, достаточно ясна.

Когда класс создается, он вводит некоторый JS в страницу Jupyter Notebook, которая использует AMD для определения mapboxglModule и, в целом, представления, представленного на основе модели, описанной в коде Python. В этом простом случае это просто текстовый элемент, но он может быть сколь угодно сложным (в конечном итоге для меня добавлена ​​карта MapboxGL).

Код также определяет свойство класса Python value, которое является словарем. При изменении значения, на которое указывает my_key, все, что отображается в выходной ячейке, будет автоматически, реактивно, обновляться!

Большая проблема: что-то где-то должно вернуть Javascript в качестве вывода, иначе это не будет выполнено в записной книжке. Вот почему есть отдельная функция create . (К сожалению, это невозможно __init__, потому что эта функция должна возвращать None). В конечном итоге я намерен предоставить размещенные записные книжки нашим пользователям через JupyterHub, и я бы, вероятно, внедрил модуль Javascript в пустой блокнот по умолчанию, вместо того, чтобы требовать от пользователя вызова кажущегося избыточным create функция.

Выход и использование

Еще одна проблема - мы используем объект dict / Javascript, на котором не будут обнаружены никакие изменения ключей - ссылка объекта указывает на то же место, поэтому он смотрит на traitlet так, как будто он не изменился. Вот почему я создал новый объект, а не просто изменил ключ существующего объекта. Сколько раз у меня были проблемы такого рода с объектами «глубокого наблюдения»? Очень много!

Как только это будет сделано, снова посмотрите на ячейку, отмеченную [75]:

Я больше не выполнял ячейку, не выполнял ее в Photoshop или какие-либо другие уловки - выходные данные ячейки буквально меняются прямо на ваших глазах.

Затем применяем этот принцип реактивности к карте Mapbox, d3, WebAudio, graphviz или networkx визуализации, а затем создаем одно бизнес-приложение, которое будет управлять ими всеми!