
Идея этой статьи родилась, когда наша команда занималась разработкой минимально жизнеспособного продукта (MVP) внутренней веб-системы, важной составляющей которой было визуальное представление данных, а именно результатов работы различных анализаторов исходного кода программного обеспечения. Из всего разнообразия библиотек визуализации в веб мы выбрали HoloViews, поскольку она в наибольшей степени соответствовала компетенциям нашей команды, костяк которой в силу специфики проекта составляли специалисты по анализу данных. Однако для успешной интеграции HoloViews в веб-приложение нам, как разработчикам, пришлось проявить и некоторую изобретательность. Мы посчитали, что имеет смысл поделиться этим опытом, поскольку в одном месте подобный материал до сих пор нигде не был собран.
При разработке веб-приложений нередко возникает потребность предоставить пользователю те или иные данные в наглядном виде. Причем желательно так, чтобы пользователь мог с этими данными взаимодействовать в своем браузере. Может показаться логичным, что для достижения подобного необходимы глубокие знания Frontend-разработки в целом и языка JavaScript в частности, которые не очень распространены среди дата-аналитиков и дата-сайентистов, программирующих, как правило, на Python. Однако с помощью библиотеки HoloViews можно достичь подобного с использованием исключительно языка Python. В статье мы расскажем о библиотеке HoloViews, позволяющей без лишних усилий создавать интерактивные графики для визуализации данных, и о том, как эти графики легко интегрировать в веб-приложение. Статья может быть интересна дата-аналитикам, дата-сайентистам, Backend-разработчикам и руководителям проектов, связанных с анализом данных.
Интерактивная визуализация данных на веб-странице
Визуализация данных — эффективный способ понимания структуры данных и выявления закономерностей в них. Как правило, для визуализации данных дата-аналитики традиционно используют либо специализированные программы, такие как SPSS, SAS, JMP, R или даже Excel, либо свободные библиотеки языка Python, такие как matplotlib, seaborn или ggplot. Отметим, кстати, что из всех языков программирования общего назначения именно Python входит в стандартный набор компетенций специалиста по анализу данных.
Часто нужно отобразить тот или иной график и на веб-странице. Статистический график может понадобиться на мощном веб-сервисе в «большом» интернете, например помесячная статистика прочтений сообщения в социальной сети или динамика курсов валют на сайте банка. Он может быть полезен и на внутреннем интернет-ресурсе предприятия. Например, возьмем графики планового и непланового ремонта и простоя оборудования на производстве или динамику количества срабатываний статического анализатора у разработчика программного обеспечения. При этом современные эстетические стандарты требуют, чтобы график выглядел на сайте органично, не нарушая его общего дизайна.
Для построения графиков внутри веб-страницы не подойдет привычный современному дата-сайентисту инструментарий визуализации данных. К счастью, существует множество библиотек, позволяющих строить графики на веб-странице, такие как D3js, C3js, Highcharts, Chart.js, Google Charts и прочее. Как правило, все такие библиотеки поддерживают более-менее стандартный набор статистических графиков: круговые диаграммы, гистограммы и столбчатые диаграммы, диаграммы рассеяния, линейные графики, «ящики с усами», тепловые карты (heatmaps) и прочее — наряду с возможностью комбинировать эти графики между собой.

При этом графики, полученные с помощью подобных библиотек, «из коробки», являются интерактивными. Они дают возможность пользователю взаимодействовать с ними. Например, показывать значения, соответствующие точке на графике, на которую наведен курсор мыши. Или просто выделять столбец гистограммы цветом, на которую указал пользователь. Нередко библиотеки интерактивной визуализации позволяют запрограммировать и сложную реакцию на действия пользователя с применением JavaScript: можно запрограммировать «перетаскивание» точки графика пользователем вместе с обновлением текстового содержимого сайта или отражать подробности при щелчке на столбец гистограммы.
Стоит отметить, что интерактивный график обеспечивает лучший пользовательский опыт, чем простой статичный график:пользователь может сам задать какие-то параметры, детальнее исследовать ту часть данных, которая ему интересна, применять фильтры к данным и т. п. Это, как правило, способствует максимальному вовлечению пользователя в процесс анализа данных и выдвижения гипотез о закономерностях в данных, становлению доверительных отношений между пользователем и разработчиком и вкупе с профессиональными знаниями пользователя в его области дает синергетический эффект, радикально повышая скорость и качество работы с данными.
Естественно, едва ли можно назвать лучшую библиотеку для интерактивной визуализации. Каждая библиотека имеет свои достоинства и недостатки. Скажем, обратной стороной большей гибкости библиотеки будет более высокий порог вхождения и сложность разработки (как в D3js), и наоборот, библиотека, которую легко изучить, может оказаться ограничивающей в плане возможных графиков (как Google Charts).
При выборе библиотеки визуализации стоит внимательно проанализировать круг задач, которые будут ей решаться: если все ограничивается стандартными графиками, то лучше использовать библиотеку попроще. Если же требуется нетривиальная визуализация со сложным взаимодействием с пользователем, то иной раз без D3 не обойтись.
Довольно крупную «ложку дегтя», с точки зрения типичного специалиста по анализу данных, добавляет то, что очень многие библиотеки визуализации данных в вебе «говорят на том же языке», что и веб-браузер, — на JavaScript. А именно, все вышеперечисленные библиотеки (D3js, C3js, Highcharts, Chart.js, Google Charts) требуют умения работать с JavaScript. К слову, эта компетенция встречается среди дата-сайентистов не так часто (в отличие от Python).
Понятно, что для крупного веб-ресурса привлечение дополнительного JavaScript-разработчика к процессу визуализации и неизбежные заботы по выстраиванию коммуникации между такими разными JavaScript-разработчиками и дата-сайентистами, возможно, и не будет проблемой на этапе вывода разработки в production. Но для небольшого проекта на внутреннего пользователя или этапа разработки MVP это может оказаться серьезным и даже непозволительным перерасходованием ресурсов.
Компромиссом может стать использование библиотеки визуализации, которая не требовала бы знания JavaScript, а работала бы на родном для дата-сайентистов Python, подобно их любимым matplotlib и seaborn. При таком раскладе, графики, запрограммированные дата-сайентистами, можно было бы без проблем включить в код, исполняемый на стороне Backend. Либо нативно, при условии что Backend работает на Python (что встречается довольно часто), либо как компонент в микросервисной архитектуре. При этом контур, в котором работает дата-сайентист, от подготовки данных до генерации графиков оставался бы подконтрольным дата-сайентисту с минимумом взаимодействий с другими разработчиками.
Такие библиотеки тоже есть: это Bokeh и HoloViews. Рабочий язык этих библиотек — Python. Но при выполнении они автоматически генерируют JavaScript-код, который строит интерактивный график, будучи открытым в браузере. Надо отметить, что, с одной стороны, HoloViews сам использует Bokeh, а с другой — значительно проще последнего в использовании. Здесь, кстати, просматривается аналогия между JavaScript-библиотеками D3js и C3js: C3js тоже более проста в использовании и тоже опирается на D3js.
В «Группе Астра» мы используем HoloViews для проектирования нашей внутренней системы отслеживания потенциальных уязвимостей в коде. HoloViews позволяет нам быстро прототипировать наши графики и быстро получать обратную связь от наших пользователей. При этом мы избегаем сложностей, связанных с интеграцией разных языков программирования, и расфокусировки внимания разработчиков, связанной с необходимостью работать на двух языках (Python и JavaScript). Со временем некоторые графики в наших веб-приложениях меняются на Frontend-библиотеки, но некоторые так и остаются в виде HoloViews.
В следующих разделах мы детально рассмотрим пакет HoloViews и интеграцию между HoloViews и веб-приложением на примере популярного веб-фреймворка Flask.
Использование библиотеки HoloViews для построения графиков в веб-приложении
Примеры графиков в HoloViews
Перед началом работы пакет HoloViews следует установить в систему. HoloViews можно установить с помощью стандартной команды:
pip install holoviews
При этом автоматически установится пакет bokeh и ряд других необходимых пакетов.
Начнем с простого примера. Построим ломаную, соединяющую точки из набора данных. В качестве набора данных будут выступать котировки акций «Группы Астра» (8 дней, с 15 марта 2024 г. по 26 марта 2024 г.).
# hvplot.py import holoviews as hv import pandas as pd hv.extension('bokeh') df = pd.DataFrame({ 'День': [1, 2, 3, 4, 5, 6, 7, 8], 'Цена акции, руб.': [561.3, 563.5, 564.2, 575.75, 611.0, 596.4, 650.2, 713.0] }) plot = hv.Curve(df) hv.save(plot, 'plot.html')
Запустив этот код, мы получим файл plot.html, открыв который в браузере, увидим:
Пользователь может делать с графиком следующее:
-
увеличивать и уменьшать масштаб;
-
выбирать область для увеличения;
-
тянуть график мышью;
-
сохранить график себе на диск в формате png.
Отметим, что мы получили такие возможности, выполнив всего лишь несколько строк кода. Что же делает код, который мы написали?
-
Для начала импортируются необходимые библиотеки. Стандартное сокращение для библиотеки
holoviews — hv. -
Строка
hv.extension...говорит о том, что HoloViews будет работать на базе Bokeh. -
Задается набор данных
df, которые мы и будем визуализировать. -
Генерируется объект
plotтипаholoviews.element.chart.Curve, содержащий наш график. -
Объект
plotсохраняется вhtml-файл.
Если открыть получившийся html-файл не в браузере, а в текстовом редакторе, то мы увидим следующие особенности файла:
-
Внутри HTML-заголовка (
<head>) присутствует загрузка JavaScript-скриптов с Bokeh, типа такого:
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-2.4.3.min.js"></script>
-
Внутри HTML-тела (
<body>) присутствует<div>и<script>.
<script> внутри <body> отвечает за собственно построение графика.
Важно, что мы сами не пишем ни HTML, ни JavaScript-код: все генерируется автоматически со стороны Python.
Аналогично можем построить другие графики.
Например, изменим наш график, задав ширину и высоту графика, толщину и цвет точки, а также добавив инструмент hover (он отображает данные при наведении на элемент графика). Для этого заменим в предыдущем примере строчку, начинающуюся на plot = hv.Curve(...) на следующую строчку:
plot = hv.Curve(df).opts(width=500, height=400, line_width=4, color='red', tools=['hover'])
Построим столбчатую диаграмму на основании тех же данных:
plot = hv.Bars(df).opts(width=500, height=400, line_width=1, color='cyan', tools=['hover'])
В качестве еще одного примера построим «ящик с усами» для нашего набора данных:
plot = hv.BoxWhisker(df['Цена акции, руб.']).opts(ylabel='Цена акции')
Множество других примеров представлено в разделах Gallery и Reference Gallery официального сайта HoloViews. Графики можно по-разному комбинировать. Библиотека HoloViews имеет много возможностей и документация по ней довольно обширна.
Казалось бы, вот мы в целом и научились строить графики в HoloViews, причем на выходе получаем HTML-файл, который можно открыть в браузере. Разве мы не достигли цели? Строго говоря, еще нет. Потребуются дополнительные усилия, чтобы интегрировать получающийся HTML-код в веб-приложение. Для этого мы познакомимся с тем, как устроено веб-приложение, и затем интегрируем в него наши HoloViews-графики.
Простое веб-приложение на Flask
Познакомимся с тем, как работает веб-приложение. Для начала установим пакет Flask:
pip install flask
Одно из простейших возможных веб-приложений будет выглядеть так:
from flask import Flask app = Flask(__name__) @app.route('/') def index_page(): html_content = "<body>Здравствуй, мир!</body>" return html_content if name == 'main': app.run(debug=True)
Запустив этот файл, получим следующее сообщение:
* Serving Flask app 'flaskapp' * Debug mode: on WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:5000 Press CTRL+C to quit ...
Пока приложение запущено, откроем URL, указанный в сообщении выше (http://127.0.0.1:5000), в любом браузере на нашем компьютере.
Не заостряя внимания на деталях, отметим, что по этому адресу будет выведено содержимое переменной html_content, которую браузер интерпретирует как HTML-страницу и покажет просто как строку «Hello world!», без тегов <body>.
Завершить выполнение Flask-приложения можно, нажав Ctrl+C.
Подробно с Flask можно познакомиться, например, с помощью Мега-Учебника Flask за авторством Мигеля Гринберга.
Интеграция Flask и HoloViews
Простой пример кода веб-приложения с графиком HoloViews выглядит следующим образом (в некотором смысле это объединение кода из предыдущих примеров по HoloViews и Flask):
Пример
# hvapp.py from flask import Flask import pandas as pd import bokeh import holoviews as hv hv.extension('bokeh') app = Flask(__name__) @app.route('/') def index_page(): df = pd.DataFrame({ 'День': [1, 2, 3, 4, 5, 6, 7, 8], 'Цена акции, руб.': [561.3, 563.5, 564.2, 575.75, 611.0, 596.4, 650.2, 713.0] }) plot = hv.Curve(df).opts(width=500, height=400, line_width=2, color='blue', tools=['hover']) bokeh_plot = hv.render(plot) plot_script, plot_div = bokeh.embed.components(bokeh_plot) html_content = f""" <body><script src="https://cdnjs.cloudflare.com/ajax/libs/bokeh/{bokeh.__version__}/bokeh.min.js"></script> {plot_script}{plot_div}</body> """ return html_content if __name__ == '__main__': app.run(debug=True)
Если мы запустим это приложение и перейдем в бразуере на страницу http://127.0.0.1:5000, то увидим следующее:
Содержимое этой веб-страницы (как исходный код, так и отображение в браузере) не сильно отличается от того, что мы уже видели в примере с HoloViews выше. Но сейчас это не просто статичная HTML-страница, а веб-страница, генерируемая веб-приложением.
Что же происходит в приложении? Проще говоря, Flask работает так, что при переходе в браузере на ссылку http://127.0.0.1:5000 вызывается функция index_page, возвращающая переменную html_content. Как и в предыдущем примере с простейшим Flask-приложением, содержимое переменной html_content — это ровно то, что получит и будет интерпретировать браузер. Что же происходит внутри index_page?
-
Задается набор данных (
df = ...). -
Строится HoloViews-график,
-
HoloViews-график переводится в Bokeh-график,
-
Далее используется метод
bokeh.embed.components, чтобы выделить компоненты<script>и<div>, знакомые нам по структуре HTML-файла, сгенерированного в разделе «Примеры графиков в HoloViews», -
Формируется переменная
html_content, включающая в себя:-
загрузку JavaScript-библиотеки Bokeh;
-
код внутри
<script>, отвечающий за график; -
HTML-элемент
<div>— место графика на веб-странице.
-
Обратим внимание на то, что в адрес JavaScript-библиотеки мы вставляем значение bokeh.__version__. Должно соблюдаться условие: версия пакета Bokeh, используемая на Frontend, должна совпадать с версией Bokeh, установленной на хосте. Таким трюком мы гарантируем, что Frontend- и Backend- версии Bokeh у нас всегда будут одинаковы.
Интеграция Flask и HoloViews: используем HTML-шаблоны
Как правило, содержимое веб-страницы, которую получает пользователь, формируется веб-приложением не с нуля, а на основе некой «заготовки», называемой HTML-шаблоном. Рассмотрим сразу на примере, как видоизменить наше имеющееся веб-приложение так, чтобы оно использовало HTML-шаблон.
В каталоге, где находится наше Python-приложение hvapp.py, создадим подкаталог templates, а в нем — файл index.html со следующим содержанием:
<!-- templates/index.html --> <body> <script src="https://cdnjs.cloudflare.com/ajax/libs/bokeh/{{bokeh_version}}/bokeh.min.js"></script> {{plot_script|safe}}{{plot_div|safe}} </body>
Наше приложение hvapp.py модифицируем следующим образом:
Модификация
# hvapp.py from flask import Flask, render_template import pandas as pd import bokeh import holoviews as hv hv.extension('bokeh') app = Flask(__name__) @app.route('/') def index_page(): df = pd.DataFrame({ 'День': [1, 2, 3, 4, 5, 6, 7, 8], 'Цена акции, руб.': [561.3, 563.5, 564.2, 575.75, 611.0, 596.4, 650.2, 713.0] }) plot = hv.Curve(df).opts(width=500, height=400, line_width=2, color='blue', tools=['hover']) bokeh_plot = hv.render(plot) plot_script, plot_div = bokeh.embed.components(bokeh_plot) html_content = render_template("index.html", bokeh_version = bokeh.__version__, plot_script = plot_script, plot_div = plot_div) return html_content if __name__ == '__main__': app.run(debug=True)
Отличие в коде от предыдущего варианта заключается только в строчке html_content = ....
Запустив это приложение и открыв адрес http://127.0.0.1:5000/ в браузере, получим точно такой же результат, что и в предыдущем случае.
Ничего не изменилось по сравнению с предыдущим шагом.
Использование HTML-шаблонов позволяет лучше организовать код, не смешивая в одном файле Frontend- и Backend-составляющие веб-приложения.
В варианте кода, использующего HTML-шаблоны, возвращаемое функцией index_page() значение html_content получается с помощью функции render_template, которая в нашем случае действует следующим образом.
-
Берется файл
index.html -
Вместо выражения
{{bokeh_version}}в HTML-шаблоне подставляется значение аргументаbokeh_versionфункцииrender_template() -
Вместо выражения
{{plot_script}}в HTML-шаблоне подставляется значение аргументаplot_scriptфункцииrender_template() -
Вместо выражения
{{plot_div}}в HTML-шаблоне подставляется значение аргументаplot_divфункцииrender_template()
Фильтр |safe после выражений plot_script и plot_div в HTML-шаблоне необходим, чтобы передаваемые значения не экранировались, а передавались как есть. Чтобы понять, зачем фильтр |safe нужен на самом деле, можно в порядке эксперимента его убрать и посмотреть, что получится: вместо графика мы увидим исходный код наших графиков, заботливо экранированный в исходном коде HTML-страницы.
Интеграция Flask и HoloViews: добавление элементов управления
Разовьем наше приложение, добавив в HTML-шаблон два элемента управления: выбор размера точки и выбор цвета.
Зададим более профессионально выглядящий **»** HTML-шаблон, добавив, помимо прочего, элементы ввода line_width и line_color:
HTML-шаблон
<!-- templates/index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>HoloViews plot app</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <style> div { padding-bottom: 1px; } </style> </head> <body> <script src="https://cdnjs.cloudflare.com/ajax/libs/bokeh/{{bokeh_version}}/bokeh.min.js"></script> <form method="post"> <div class="container"> <div class="row"> <label class="col-sm-2">Размер точки:</label> <div class="col-sm-2"> <input name="line_width" class="form-control" type="number" value="{{line_width}}"/> </div> </div> <div class="row"> <label class="col-sm-2">Цвет точки:</label> <div class="col-sm-2"> <select name="line_color" class="form-control"> <option value="red" {% if line_color=="red" %}selected{% endif %}>Красный</option> <option value="blue" {% if line_color=="blue" %}selected{% endif %}>Синий</option> <option value="green" {% if line_color=="green" %}selected{% endif %}>Зеленый</option> <option value="black" {% if line_color=="black" %}selected{% endif %}>Черный</option> <option value="yellow"{% if line_color=="yelllow" %}selected{% endif %}>Желтый</option> </select> </div> </div> <div class="row"> <div class="col-sm-2 offset-sm-4"> <input class="form-control btn-success" type="submit" name="submit_button" value="Построить"> </div> </div> <div class="row"> <div class="col-sm-6"> {{ plot_div|safe }} {{ plot_script|safe }} </div> </div> </div> </form> </body> </html>
Основных отличий от предыдущего варианта здесь три:
-
В строчке
@app.route...в скобках добавлен параметрmethods=['GET', 'POST']. Это необходимо для того, чтобы правильно отрабатывалось нажатие на кнопку «Построить». -
Введены переменные
line_widthиline_color. -
Добавлена отработка нажатия на кнопку «Построить»
if request.method == 'POST'.
Смысл внесенных изменений в том, что при нажатии кнопки «Построить» приложение запрашивает значения, введенные пользователем в интерфейсе, и присваивает их параметрам point_size и point_color, которые, в свою очередь, используются при построении графика.
Что касается HTML-шаблона, то он теперь, помимо элементов <scrip> и <div> графика, содержит две элемента управления (<select> и <input>), позволяющих выбирать размер и цвет. А также саму кнопку «Построить».
Дальнейшее развитие веб-приложения
Это веб-приложение можно развивать дальше. Например, мы можем добавить выбор типа графика (ломаная, столбчатая диаграмма или «ящик с усами») и приложение будет выглядеть так:
Таким же образом в веб-приложении можно добавить выбор источника данных, фильтрацию данных по тому или иному критерию и т.д. В итоге мы имеем возможность построить очень развитое веб-приложение.
Заключение
Благодаря HoloViews наша небольшая команда смогла разработать MVP веб-системы своими силами, без привлечения к разработке специалистов по Frontend. Наши аналитики данных могли свободно экспериментировать со способами визуального представления данных прямо в веб-приложении. Это позволило быстро получать обратную связь от пользователей, что, в свою очередь, положительным образом сказалось на скорости разработки.
В этой статье мы рассмотрели способы построения интерактивных графиков внутри веб-приложения. Детально был рассмотрен способ интеграции веб-фреймворка Flask с библиотекой визуализации HoloViews, который доступен Python-разработчикам без необходимости осваивать JavaScript. Если говорить вкратце, этот способ сводится к тому, что график HoloViews сначала преобразуется в объект Bokeh, из которого вычленяются компоненты <script> и <div>, далее они уже отправляются на Frontend в составе HTML-страницы.
Отметим, что такая схема интеграции HoloViews также подходит для ряда других веб-фреймворков, в частности Django и Vue: общая схема остается неизменной, а в деталях отличается лишь способ передачи кода для <script> и <div> (важно следить за тем, чтобы передаваемые переменные не экранировались, а также за совпадением версий Bokeh на Frontend и Backend).
На этой странице приводится альтернативный способ комбинации HoloViews и Flask, однако рассматриваемый там способ предъявляет более высокие требования к уровню технической экспертизы читателя, в отличие от настоящей статьи, и подходит не для всех реалий разработки.
ссылка на оригинал статьи https://habr.com/ru/articles/814881/
Добавить комментарий