Идея этой статьи родилась, когда наша команда занималась разработкой минимально жизнеспособного продукта (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, однако рассматриваемый там способ предъявляет более высокие требования к уровню технической экспертизы читателя, в отличие от настоящей статьи, и подходит не для всех реалий разработки.
omaxx
А как альтернативу Plotly Dash не рассматривали?
AstraLinux_Group Автор
Да, рассматривали. Наша задача была не только в том, чтобы сделать интерактивную визуализацию в веб, но и интегрировать графики во Flask-приложение. Вот здесь, например, расписано, как вложить Dash во Flask — мы посчитали, что вложить HoloViews во Flask будет проще.
AstraLinux_Group Автор
В предыдущем сообщении прикрепили неверную ссылку, вот нужная: https://hackersandslackers.com/plotly-dash-with-flask/