Руководство для новичков по созданию интерактивных информационных панелей: приложение Bokeh в реальном времени
В первых двух частях серии мы многое узнали о боке. Мы уже знаем, как создавать автономные документы с глифами Bokeh, как встраивать их в записные книжки Jupyter, настраивать и добавлять взаимодействия. Что еще более важно, мы узнали, как разрабатывать базовые приложения Bokeh и запускать их с помощью сервера Bokeh. Первые две части можно найти здесь: Часть I, Часть II.
Обладая всеми знаниями, мы можем построить что-то реальное. В этом руководстве мы разработаем интерактивную панель управления для визуализации вызовов службы экстренной помощи в Сиэтле. Более того, это приложение не только будет отображать некоторые статические данные, но и будет регулярно получать новые данные и соответствующим образом обновлять информационную панель.
Разделение проблем
Прежде чем углубляться в разработку нашего приложения, давайте согласимся с важным архитектурным принципом: уровень данных должен быть четко отделен от уровня представления. Хотя это общеизвестный и уважаемый принцип в разработке программного обеспечения, этот тип разделения критически важен для приложений приборной панели. Почему?
Приложения с многофункциональной панелью управления могут выполнять множество операций с данными под капотом, например выборку, преобразование и объединение данных из нескольких источников. Без разделения между уровнями приложения любые изменения в том, как собираются или преобразуются данные, или даже в том, какие данные используются, могут быстро просочиться в части приложения, связанные с построением графиков, взаимодействием и даже стилизацией, что приведет к массивным и беспорядочным изменениям кода.
К счастью, Bokeh предоставляет нам идеальные инструменты для отделения данных от их представления: столбчатые источники данных и представления. Мы создадим простой и автономный поставщик данных, который будет отслеживать все изменения данных и предоставлять средства построения графиков с соответствующими источниками данных и представлениями.
Мы создадим приложение с очень общим макетом и стилем. Приложения Bokeh - это не просто сценарии Python, они могут содержать шаблоны, файлы CSS, пользовательские темы и многое другое. Шаблоны и настраиваемые темы служат той же цели, что и общий шаблон MVC: они позволяют четко разделить то, как подготавливаются графики, и как они фактически представлены на веб-странице и стилизованы.
Эти инструменты добавляют гибкости, и мы будем широко использовать их в нашем приложении, чтобы сделать его еще более модульным.
Предварительный просмотр панели инструментов
А теперь давайте создадим его!

Дизайн приложения
Для этого приложения мы будем использовать набор открытых данных, предоставленный городом Сиэтл. Данные содержат 911 отправлений и обновляются каждые 5 минут. И структура данных, и доступ к ним достаточно просты, а постоянное обновление данных позволит нам практиковаться в разработке всей инфраструктуры, необходимой для динамических панелей мониторинга.
У нас нет требований к клиенту для создаваемого приложения, поэтому мы должны сами попытаться представить разумный дизайн приложения.
Итак, давайте разберем информацию, которая у нас есть о каждом звонке в службу экстренной помощи:
- местонахождение и адрес отправки,
- дата и время,
- тип.
Для каждого вызова эти три элемента данных отвечают на три вопроса: «Что произошло? Где это произошло? Когда это произошло? ». Теперь давайте обрисуем, как мы можем представить данные и какую агрегированную информацию мы можем захотеть представить вместе.
Место отправки
Первая и наиболее очевидная идея - визуализировать отправления на карте. Достаточно разумно. Это позволило бы нам быстро оценить, в какие части города отправляются депеши службы экстренной помощи, а также дать визуальное представление о географической плотности отправки. Это представит аспект данных «Где?».
Затем, поскольку мы отображаем отправления на карте, нам следует подумать о добавлении всплывающих подсказок с подробной информацией, чтобы пользователь мог посмотреть на конкретный вызов и понять, какой это тип вызова, точное время, когда звонок был отправлен и адрес, на который был отправлен звонок. Это дополнило бы пространственный аспект данных.
Измерение времени
Наличие динамически обновляемой карты - это здорово, но мы можем и должны отображать ту же информацию в какой-то другой форме. Карта предоставляет только пространственное представление данных, но данные имеют другие измерения, самое главное - время.
Представьте себе, что звонки поступают один за другим в течение нескольких минут: это будет свидетельствовать о достаточно большой нагрузке на систему. Но совсем другое дело, если между звонками десятки минут. Конечно, это маловероятно для города такого размера, но мы стараемся охватить как можно больше реалистичных сценариев, чтобы наше приложение можно было использовать в любом из них.
Взглянув, скажем, на таблицу, в которой одна за другой перечислены все депеши, мы можем быстро оценить текущую нагрузку на службы экстренного реагирования Сиэтла. Это представление предоставит «Когда?» , а также дать более общую картину данных.
Типы аварийных ситуаций
Хотя это критически важно для полезного приложения, места и времени все же недостаточно. У нас есть еще один важный компонент данных: тип отправки. Распределение типов рассылки может предоставить важную информацию о текущих событиях. Легче всего понять гистограмму со статистикой типа отправки.
Кстати, если вы хотите получить некоторые специализированные знания в предметной области и более глубокое понимание, вы можете перейти на обмен сообщениями пожарного управления Сиэтла. На странице также есть подробное описание типов отправлений.
Подведем итоги дизайнерских идей, которые мы реализуем:
- мы будем использовать участок карты для отображения мест отправки, и этот график будет иметь значимые всплывающие подсказки, чтобы предоставить некоторую дополнительную информацию о доставке,
- дополнительная таблица будет отображать отправления в текстовой форме, и эта таблица должна позволять сортировку по любому столбцу,
- третий элемент приложения - это столбчатый график, отображающий статистику типов отправки.
Механизм фильтрации
У нас пока нет механизма фильтрации. Следует ли отображать отправку за последний час? Двенадцать часов? Весь день? В зависимости от потребностей пользователя это число может отличаться, поэтому лучший способ удовлетворить это требование - выбрать разумное значение по умолчанию и позволить пользователь меняет это.
Таким образом, нам нужен ползунок для установки количества часов. Для простоты давайте установим 1 час в качестве значения по умолчанию и 168 часов (т. Е. Одну неделю) в качестве максимального значения и позволим выбрать любое количество часов в этом интервале. При запуске приложения мы отобразим 911 звонков за последний час.
Обладая этими знаниями, мы уже можем построить каркас для нашего приложения.
Выкладывать вещи
Раньше мы использовали внутреннюю функциональность Bokeh для создания макета приложения. На этот раз мы пойдем дальше и добавим в приложение полноценный шаблонизатор. Боке позволяет легко настраивать шаблоны. Нам нужно только создать действительные шаблоны, поместить их в нужное место и встроить модели боке в шаблон.
Приложение Bokeh может быть одного из двух типов: оно может содержаться в одном файле или может занимать несколько файлов как приложение каталога. Приложения каталога в Bokeh требуют определенного макета каталога: с файлом приложения с именем main.py в корневом каталоге и пользовательскими шаблонами, расположенными в <app directory>/templates.
Мы также добавим отдельный файл конфигурации для опций конфигурации всего приложения, чтобы все было немного чище. Создадим все необходимые файлы и каталоги:
Результирующий макет каталога выглядит следующим образом:
Действительно, у нас есть действующее приложение каталога Bokeh, и мы можем его обслуживать:
При запуске Bokeh печатает ссылку на сервер разработки, и вы можете следить за ней, чтобы посмотреть на наше недавно появившееся (и пустое) приложение Bokeh.
Обработка данных
Socrata API
Данные, которые мы будем использовать, предоставляются через интерфейс Socrata. Socrata - это система баз данных, широко применяемая государственными и муниципальными учреждениями как для внутреннего, так и для внешнего использования, и очень часто является основным и самым простым способом получения открытых данных. Чтобы использовать Socrata из Python, нам сначала нужно установить пакет sodapy, который обеспечивает привязку Python к Socrata:
Получить данные из Socrata API просто: нам нужно знать только исходный домен и идентификатор набора данных. Перейдите в API› API Docs на странице Seattle Real Time Fire 911 Calls и просмотрите документацию по Socrata:
Теперь мы настроены на получение данных:
Давайте разберем запрос.
Сначала команда sodapy.Socrata(domain, app_token) создает клиента Socrata. Обратите внимание, что мы не используем токен приложения: вы можете получить его на соответствующей странице, но доступ открыт даже без токена приложения, хотя и с некоторыми ограничениями регулирования.
Во-вторых, строка client.get(dataset_indentifier) выполняет фактический запрос данных. client.get поддерживает запросы, подобные SQL, и мы будем использовать эту функцию позже для фильтрации запросов по дате и времени. Результат представлен в виде списка словарей, и каждый словарь содержит все необходимые нам поля:
Нам понадобятся данные в виде apandas DataFrame, поэтому давайте сразу же их преобразуем:
Это все, что нам нужно знать о Socrata, чтобы создать желаемое приложение. Однако для использования в приложении самих данных требуется немного больше работы, и мы добавим дополнительные шаги предварительной обработки позже.
Поставщик данных
Теперь мы знаем, как получать данные от Socrata, но как насчет всех контейнеров и представлений, которые нужны нашему приложению Bokeh? Прежде чем углубиться в код, давайте потратим немного времени на разработку поставщика данных:
- он должен содержать средства получения данных,
- полученные данные будут храниться как для использования Bokeh, так и для вычислений, таких как получение статистики по типам ответов. Как вы помните, мы также хотим, чтобы данные постоянно обновлялись, а это значит, что мы будем широко использовать источники данных и представления Bokeh,
- поставщик данных должен обновлять просмотры на основе выбранного количества последних часов для отображения,
- нам нужен поставщик данных для расчета статистики типов отправлений.
Для ясности, некоторые из описанных выше функций будут реализованы несколько упрощенным способом, но главное предостережение, о котором мы должны помнить, заключается в том, что в реальном приложении у нас будет какой-то тип постоянного хранилища и нетривиальная внутренняя диспетчеризация данных. Но в этом руководстве мы будем придерживаться простой реализации в памяти.
Итак, давайте создадим модуль для поставщика данных:
API поставщика данных отражает требования:
Хотя docstrings не требует пояснений, мы все же должны объяснить параметры инициализатору класса:
- source и dataset_id являются источником набора данных и идентификатором набора данных, то есть data.seattle.gov и kzjm-xkqj; мы делаем это настраиваемым, чтобы поставщик данных мог гибко реагировать на возможные изменения либо местоположения набора данных, либо идентификатора,
- n_types - количество типов рассылки, для которых нужно рассчитать статистику; мы не хотим, чтобы наша гистограмма была загромождена, поэтому мы будем отображать только n_types наиболее частые типы,
- hrs - текущее количество последних часов, а max_hrs - максимальное количество часов из прошлого (168 часов, как мы обсуждали ранее); здесь мы воспользуемся уловкой, чтобы значительно упростить поставщика данных: при инициализации поставщик данных извлечет все данные до max_hrs в прошлое. После этого нам нужно будет только получать обновления и фильтровать данные по мере необходимости без сложных или частичных выборок.
Позже вы поймете, зачем нам update_filter для расчета статистики типа отправки и как она будет использоваться. Мы готовы написать код поставщика данных, но нам все еще нужно решить незначительные проблемы.
Координаты обработки
Координаты в используемом наборе данных представлены как условные широта и долгота. Легко и просто? Не совсем. Для участков карты в боке требуются координаты проекции Меркатора, хотя боке по-прежнему обозначает оси в единицах широты и долготы. Таким образом, нам нужно сначала преобразовать координаты.
Для реализации преобразования мы будем использовать GeoPandas. Чтобы выполнить фактическое преобразование, все, что нам нужно знать, это система координат (CRS) исходных данных и графика карты в боке: достаточно просто, это EPSG: 4326 и EPSG: 3857 соответственно.
Тем не менее, мы реализуем преобразование более общим способом, позволяющим преобразовывать из любой исходной CRS в любую целевую CRS, поскольку это почти ничего не стоит, но может быть полезно в другом месте или в случае каких-либо изменений в Bokeh API:
Давайте немного разберем этот код. Сначала мы извлекаем координаты из исходного DataFrame и преобразуем их в числовые значения:
pd.to_numeric здесь, потому что Сократа вернет все значения в виде строк. Затем мы создаем фактический DataFrame GeoPandas:
Осталось только реальное преобразование, которое легко достигается с помощью coords.to_crs.
Обработка часовых поясов
Еще одно второстепенное решение связано с часовыми поясами. Отправления службы спасения в Сиэтл отправляются в часовой пояс США / Тихоокеанский регион. В зависимости от потребностей пользователя мы можем захотеть преобразовать формат даты в местное время пользователя (или UTC) или оставить его как есть.
Для упрощения мы будем использовать исходный неизмененный часовой пояс. Единственная точка в нашем приложении, где мы будем указывать часовой пояс, - это фильтры для наших данных. Однако в другом месте мы будем использовать наивные объекты даты и времени с часовыми поясами, так как они лучше всего работают с боке.
Инициализация поставщика данных
Пока что мы позаботились о преобразовании координат в нашем провайдере данных. Давайте продолжим и полностью реализуем поставщика данных с учетом всех требований, изложенных выше.
Во-первых, давайте создадим инициализатор для поставщика данных.
Обратите внимание, что в блоках кода мы предоставляем только постепенные изменения кода, а полный код будет предоставлен для загрузки в конце сообщения.
Итак, в инициализаторе выставляем все необходимые поля и создаем все контейнеры:
Код не требует пояснений, за исключением start_time вычисления. Обратите внимание, что при первой выборке мы получаем все данные за максимальное количество часов. При каждой следующей загрузке нам будут нужны только бесплатные обновления.
Мы можем отложить создание контейнеров, но если мы создадим их здесь, мы сможем инициализировать поставщик данных с помощью метода fetch_data и пропустить любой пользовательский код инициализации.
Также обратите внимание, что мы пытаемся сделать вещи немного чище, добавляя атрибуты класса (COLS и RAW_COLS), так что нам не нужно постоянно указывать полный список столбцов.
Получение данных
Теперь приступим к загрузке данных:
Фактическая выборка из Socrata выполняется с использованием client.get с предложением where, которое работает точно так же, как вы ожидаете от языка, подобного SQL: вы предоставляете условие или список условий в виде строки. Обратите внимание, что мы сортируем результат по datetime, так как при этом наш data контейнер будет всегда упорядочен, когда мы добавляем новые потоки данных.
После получения данных от Socrata мы создаем из него фрейм данных и проверяем, действительно ли у нас есть какие-то новые данные. Если мы это сделаем, мы можем перейти к подготовке данных: во-первых, нам нужно преобразовать пространственные координаты, чтобы мы могли нанести их на карту, во-вторых, мы гарантируем, что datetime действительно a datetime, а не строку, а затем мы просто добавляем только что выбранные данные в источник данных.
Вы можете спросить, зачем нам else пункт. Это сложный вопрос, требующий некоторых знаний о боке. Проблема в том, что мы делаем stream для нашего источника данных при инициализации. На этом этапе на нее не подписываются никакие модели. В результате этот первый поток может не распространяться на модели позже, и мы можем получить несинхронизированные обновления моделей.
Таблицы - первые кандидаты, которые от этого пострадают. Таким образом, мы добавляем тривиальный stream, который будет вызываться даже при отсутствии данных, что, в свою очередь, заставит все обновиться и синхронизироваться после того, как все модели будут подписаны.
Обновление фильтра
На данный момент нам все еще нужно обновить представление источника данных, чтобы все вызовы, которые произошли более hrs часов назад, были отфильтрованы. Это делается с помощью update_filter, который мы вскоре создадим.
После этого нам необходимо обновить статистику типа отправки, так как она также отражает статистику за последние hrs часов.
Через мгновение вы поймете, зачем нам нужен
time_filter, но короче говоря, он используется для упрощения: в любом случае мы рассчитываем его для представления источника данных, почему бы не использовать его повторно?
Итак, метод update_filter будет работать так:
Сначала он получает текущее время в часовом поясе Сиэтла, а затем просто отфильтровывает все вызовы, которые произошли более self.hrs назад. Внутри представления источника данных data_view мы обновляем фильтры напрямую: Bokeh будет отслеживать это и выполнять все необходимые действия на стороне клиента.
Обновление статистики типов отправлений
Давайте продолжим и запрограммируем метод update_stats:
Теперь вы понимаете, почему мы использовали time_filter трюк: он позволяет нам упростить подсчет значений с помощью логической индексации в data фрейме данных. После этого просто пополняем type_stats_ds новыми данными.
Вы можете спросить: почему мы не используем здесь какой-то продвинутый метод для обработки обновлений
type_stats_ds?
Установка исправлений может быть сложной, поскольку у нас может быть менее
n_typesразличных типов (представьте, что за последний час было всего 4 разных типа отправки). Было бы проще использовать этот источник данных напрямую и обновить глиф в другом месте.
Реализация set_hours
Последнее, что нам нужно в поставщике данных, - это функциональность для hrs обновления:
В этом методе мы устанавливаем границы с помощью np.clip, а затем обновляем статистику типов фильтрации и отправки.
Поздравляем, мы завершили самую сложную часть нашей заявки! Найдите минутку, чтобы поиграть с поставщиком данных и понять, как он работает.
Конечно, мы все еще можем улучшить поставщика данных и сделать код более надежным. Есть несколько способов злоупотребления этим кодом. Например, это небезопасно для многопоточности. Также представьте, что кто-то попытается вызвать
update_filterбезupdate_stats, что приведет к несогласованному отображению внутренних контейнеров.
Код приложения
Теперь, когда у нас есть весь необходимый код управления данными, мы можем начать с самого приложения. Подведем итоги того, что у нас есть в нашем файле конфигурации:
Некоторые из этих параметров конфигурации будут использоваться для фактического создания поставщика данных внутри приложения.
HTML-шаблон приложения
Для макета приложения мы будем использовать шаблон HTML, а не внутренние инструменты Bokeh (например, column или row). Пользовательский HTML-шаблон обеспечивает большую гибкость и позволяет нам использовать любой стиль, который мы хотим.
Первый инструмент, который мы будем использовать в нашем шаблоне, - это CSS-фреймворк Bootstrap. Мы будем использовать его, чтобы красиво разместить элементы приложения в сетке. В шаблонах Bokeh используется шаблонизатор Jinja, поэтому добавить в HTML какие-либо пользовательские элементы не проблема. Если вы знакомы с веб-разработкой, вам будет очень знаком приведенный ниже шаблон:
Bokeh предоставляет базовый шаблон, который следует расширить в пользовательских приложениях. В этом базовом шаблоне есть разделы для правильного размещения нестандартного оборудования.
Например, раздел preamble переходит к head элементу последней HTML-страницы, а раздел content переходит к body.
Во время рендеринга Jinja соберет для нас финальную HTML-страницу за кулисами. Хотя Jinja - чрезвычайно мощный движок для создания шаблонов, мы будем использовать только два его инструмента для создания приложения Bokeh с пользовательскими шаблонами: блоки и макрос embed.
Давайте разберем шаблон. В разделе preamble мы добавляем Bootstrap CSS через ссылку CDN. Сделав это, мы можем использовать все функции Bootstrap внутри нашего шаблона. Итак, мы делаем именно это с классами row и col-, размещая элементы в сетке.
Объяснение 12-столбцовой сетки в Bootstrap выходит за рамки этой серии, но общая идея очень проста: мы помещаем элементы в контейнеры и указываем их ширину, используя относительные количества (таким образом, col-6 занимает половину родительского контейнера). Различные устройства и разрешения экрана обрабатываются сеточной системой Bootstrap. Если вы не знакомы с Bootstrap, проверьте документацию.
Еще один важный элемент - макрос embed. Это позволяет нам встроить модель в HTML-страницу. Во время рендеринга {{ embed(roots.main_plot) }} будет заменен моделью с именем main_plot (или, если быть более точным, клиентским двойником модели Python). Итак, нам нужно присвоить моделям имена, чтобы их можно было встроить в шаблон HTML.
Модели приложений
Теперь у нас есть код управления данными и HTML-шаблон приложения. Создание самого приложения теперь довольно просто. Начнем с таблицы для текстового представления данных:
Мы создаем таблицу в соответствии с шаблоном, который мы описали во второй части серии статей, с той лишь разницей, что в программе форматирования для поля datetime. Мы заставляем табличное представление скрывать индекс строки, так как он нам не нужен. Обратите внимание, что мы добавляем name=”table”, чтобы таблица могла быть обнаружена механизмом шаблонов.
Далее идет сюжет карты:
Для построения карты мы добавляем некоторые специфические параметры конфигурации. Сначала мы указываем типы осей withx_axis_type=”mercator”, y_axis_type=”mercator”. Во-вторых, мы определяем оси x и y, чтобы они имели одинаковые единицы экрана с match_aspect=True, поскольку мы не хотим, чтобы карта искажалась.
Чтобы на графике были настоящие плитки, нам нужно выбрать поставщика плитки. Мы будем использовать CartoDB: он бесплатный и визуально выглядит прекрасно. Вызовы отображаются в кружках на карте, и мы предоставляем как источник данных, так и представление данных, так что и таблица, и карта отображают одни и те же события в течение выбранного количества часов.
Единственная оставшаяся модель - это гистограмма для статистики типа отправки:
Обратите внимание, что для этого типа графика нам нужно указать x_range=data_provider.dispatch_types, чтобы Боке знал, что нам действительно нужна категориальная ось X. Под капотом Боке создаст то, что называется aFactorRange для размещения категориальных данных.
Управление приложениями
Теперь мы в одном шаге от запуска нашего приложения. Последнее, что нам нужно для доработки макета, - это добавить ползунок для выбора количества часов. Давайте добавим один:
Теперь, когда у нас есть все элементы, мы можем добавить их в документ по умолчанию с помощью
Давайте запустим наше приложение в режиме разработки (чтобы Bokeh перезагружало приложение при любом изменении кода) и посмотрим, что у нас есть на данный момент:

Как видите, наше приложение фактически отображает весь макет. Он по-прежнему статичен, потому что мы не подключили никаких обратных вызовов. Мы сделаем это в ближайшее время. Еще один элемент, которого не хватает нашему приложению, - это стиль. Мы поработаем над этим в конце этого урока и сделаем нашу панель управления гладкой и визуально легкой.
Чтобы сделать наше приложение действительно динамичным, нам нужно создать периодический обратный вызов, который будет запускать регулярные обновления отображаемых данных. На стороне поставщика данных обновления обрабатываются fetch_data, поэтому давайте создадим функцию обратного вызова и подключим ее к документу.
Сначала мы добавим интервал обновления в файл конфигурации:
Затем мы создаем функцию и подключаем ее к документу как периодический обратный вызов. Может показаться, что нам нужно только регулярно звонить data_provider.fetch_data . Но нам также необходимо обновить график статистики типа отправки, что нужно сделать вручную (хотя соответствующий источник данных создается и поддерживается поставщиком данных).
Помните, мы сделали это, чтобы обойти любое сложное исправление, и теперь мы платим за это гораздо меньшую цену.
Обновить график статистики типа отправки на самом деле довольно просто: нам нужно изменить только коэффициенты X -axis (поскольку они могут меняться со временем). Обновления источника данных для статистики типа отправки выполняются внутри поставщика данных и распространяются Bokeh без нашего вмешательства.
Если мы не обновим диапазон X -axis, Bokeh будет просто распространять обновления данных из базового источника данных, и данные глифов будут обновлены, а коэффициенты X -axis будут остаются в своем предыдущем состоянии, которое может отражать или не отражать обновленные типы рассылки Топ-10.
Далее следует обратный вызов слайдера. Ползунок предназначен для изменения количества отображаемых последних часов. Как и любой другой виджет, он предоставляет обратный вызов on_change(attr, old, new). В этом случае attr равно "value" (текущее значение ползунка), и нам нужно вызывать метод set_hrs в нашем поставщике данных всякий раз, когда изменяется значение ползунка:
Обратите внимание, что мы снова заставляем обновить график статистики типа отправки.
Пока что весь функционал нашего приложения на месте. Вы можете запустить его или перезагрузить вкладку браузера, если вы запустили сервер Bokeh с --dev.
Поиграйте с приложением на мгновение, чтобы определить, что-то отсутствует или работает не так гладко, как ожидалось. Приложение теперь работает и содержит все функции управления данными и построения графиков. Осталось только стилизация. Теперь мы доработаем приложение: создадим собственные стили и тему и добавим небольшие изменения в макет.
Укладка
Есть несколько способов настроить стиль приложений Bokeh. Мы будем использовать несколько из них, потому что нам нужно настроить общий вид, графики и таблицу одновременно.
Пользовательская тема и макет
Начнем с пользовательской темы. Боке ожидает, что это будет theme.yaml, поэтому давайте создадим его:
Внутри файла темы Bokeh ожидает атрибуты для различных моделей, которые вы хотите изменить для всего приложения. Конечно, это вопрос личных предпочтений, и мы рекомендуем вам настроить его в соответствии с вашими, но мы хотим сначала стилизовать по крайней мере следующее:
Файл темы прост: он содержит атрибуты для каждой модели, которые вы хотите установить, но не хотите делать это в коде Python.
Обратите внимание, что мы добавляем
FactorRangeмодель отдельно, чтобы она применялась только к графику статистики типа диспетчеризации, чтобы освободить больше места вокруг столбцов.
Для нашего виджета таблицы мы предоставляем более конкретные ограничения, чтобы он хорошо вписывался в размер сетки HTML.
К сожалению, невозможно указать атрибуты для осей X и Y отдельно в файле темы. Итак, чтобы повернуть метку оси X- для stats_plot, мы должны прибегнуть к коду Python:
Фактически мы минимизировали информацию о стилях в коде Python и переместили ее в файл темы, улучшив модульность приложения и отделив слой стиля от слоя представления.
Было бы здорово установить атрибуты по имени модели, а не только по типу модели, конечно, но такая функциональность еще не реализована, поэтому мы должны указать любые атрибуты для каждого графика в коде.
Пока наше приложение выглядит так:

Это уже неплохо, но есть еще много вещей, которые мы можем улучшить. Начнем с добавления небольшого интервала к макету. Для этого мы добавим классы непосредственно к HTML-элементам. Bootstrap предоставляет набор классов mx- и my-. Они устанавливают горизонтальные или вертикальные поля. Давайте добавим my-4 к первым двум row элементам нашего HTML-шаблона:
Пользовательский шрифт
Следующим очевидным шагом будет изменение шрифтов. Мы будем использовать шрифт Quicksand через шрифты Google, но вы можете предпочесть другой собственный шрифт. Дело в том, чтобы продемонстрировать подход в целом. Чтобы настроить шрифт для всего приложения, нам нужно сделать несколько вещей. Сначала мы добавляем Quicksand из Google Fonts как <style> элемент в preamble раздел нашего HTML-шаблона:
Затем мы устанавливаем новый пользовательский шрифт в нашем theme.yaml для меток:
Чтобы заставить все другие элементы использовать Quicksand, мы будем использовать ярлык: мы знаем, что Bokeh встраивает свои элементы в <div> элемент с классом bk-root. Хорошо, давайте воспользуемся этим и добавим еще одну запись в только что созданный раздел <style> в преамбуле:
Теперь, наконец, это все зыбучие пески. Это не самый чистый способ, но, безусловно, самый простой. Наше приложение начинает обретать форму.

Всплывающие подсказки
Новый шрифт добавляет стиль приложению, но посмотрите наши всплывающие подсказки на карте:

Совсем не информативно. Давайте проведем рефакторинг, чтобы отобразить что-нибудь полезное. Для этого в нашем main.py мы создадим собственный шаблон всплывающей подсказки:
Bokeh позволяет указывать поля данных в таких шаблонах. Во время рендеринга (т. Е. Когда вы наводите курсор на глиф, к которому прикреплена эта всплывающая подсказка) @type будет заменен фактическим типом отправки, а также другими полями данных. Чтобы прикрепить эту подсказку к инструменту наведения на графике карты, мы делаем следующее:
Мы сделали три простых вещи:
- убран наведение из списка
tools, - создал его вручную с помощью настраиваемого шаблона всплывающей подсказки,
- и прикрепил вновь созданный инструмент к сюжету.
Давайте также добавим немного места вокруг текста с помощью собственного стиля:
Теперь всплывающие подсказки выглядят намного лучше:

Стилизация виджета таблицы
Чтобы завершить оформление, мы можем захотеть изменить способ визуализации таблицы. Для визуализации таблиц Bokeh использует вариант SlickGrid, в котором определен собственный набор классов. Чтобы изменить внешний вид заголовков столбцов, мы добавляем новые записи в наши пользовательские стили:
Вы можете спросить, как мы узнаем, какие классы нужно изменить. Нет никаких правил, так как Bokeh использует множество различных классов, поэтому, если вы хотите что-то изменить, вы переходите к файлам Bokeh CSS (например, здесь) и пытаетесь найти, какой класс отвечает за визуальный атрибут, который вы хочу изменить.
В качестве последнего штриха к приложению давайте добавим пояснительный текст сразу после названия приложения:
Бонус: использование иконок Font Awesome
Хотя наше приложение полностью функционально и стилизовано, мы можем добавить последний штрих и использовать некоторые значки из коллекции Font Awesome, чтобы добавить визуальную подсказку к типам отправки. Мы добавим значки для некоторых видов медицины, автомобильных происшествий и пожаров.
Перед тем, как начать использовать Font Awesome, нам нужно включить соответствующий CSS:
Итак, что мы хотим получить? В столбце таблицы, обозначающем тип отправки, нам нужен не только текст, но и соответствующий значок. Для этого нам сначала нужно рассчитать, какой значок использовать. Для этого мы добавим в наш шаблон пользовательскую функцию JavaScript:
Как видите, эта функция довольно проста: сначала она проверяет тип, а затем выдает HTML-код, содержащий соответствующий значок (посмотрите, как они выглядят, в Галерее Font Awesome).
Затем ячейки таблицы позволяют настраивать форматирование HTML. HTMLTemplateFormatter отвечает за это. Более того, внутри HTML-шаблона для ячейки мы можем предоставить шаблон JavaScript с Underscore.js.
Посмотрим, как это работает. Наш HTML-шаблон ячейки будет выглядеть так:
Во время рендеринга, если он прикреплен к столбцу, который соответствует type, это средство форматирования распознает, что <%= … %> является шаблоном, и выполнит любой имеющийся там JavaScript. Чтобы прикрепить это средство форматирования к столбцу type в нашей таблице, мы просто указываем его при создании столбца, т.е. мы создаем столбец, подобный этому
вместо того
Вот и все. Теперь все будет происходить само собой. К сожалению, нет простого способа добавить значки к всплывающим подсказкам на графике карты, потому что пользовательские всплывающие подсказки JavaScript экранируют свое содержимое, и мы получим элемент <i> в виде простого текста вместо фактического значка.
Выводы
После всех доработок и доработок наше приложение выглядит отлично:

Чтобы увидеть, как это работает после развертывания, вы можете посетить Сиэтл 911 звонки, а код приложения доступен на GitHub.
Наша панель управления выполняет свою работу, она стильная и совсем не такая большая. В нем даже есть связанные выборки между таблицей и графиком карты, и они доступны бесплатно!
Обратите внимание, что большая часть кода отправляется поставщику данных, а не графику или макету.
Заворачивать
Мы потратили много времени на изучение боке и построение различных сюжетов и макетов. Вы узнали, как создавать простые и не очень простые графики в Bokeh, как использовать их в записных книжках Jupyter, и, что наиболее важно, как создавать автономные приложения с Bokeh, от самых простых до довольно сложных.
Однако это не конец истории. Часто интерактивные информационные панели не являются продуктами сами по себе, а служат строительными блоками для более крупных приложений. К счастью, сервер Bokeh достаточно гибок, чтобы быть встроенным в приложения Flask или Django, а с объединенной мощью Python и JavaScript вы можете достичь любой сложной цели.
Надеюсь, после прохождения этой серии руководств у вас будет прочная основа для погружения в расширенные функции боке.