<НА ГЛАВНУЮ

Интерактивный дашборд в реальном времени с Bokeh и CustomJS

'Гайд по созданию интерактивного дашборда на Bokeh с клиентскими CustomJS-колбэками, фильтрами, цветовой картой и стримингом данных.'

Установка и импорт

Сначала установим и импортируем библиотеки, необходимые для создания интерактивных визуализаций в Bokeh и работы с данными.

!pip install bokeh pandas numpy scipy -q
import numpy as np
import pandas as pd
from bokeh.io import output_notebook, show, export_png, output_file
from bokeh.plotting import figure
from bokeh.layouts import row, column, gridplot
from bokeh.models import (
   ColumnDataSource, HoverTool, LassoSelectTool, BoxSelectTool, TapTool,
   ColorBar, LinearColorMapper, BasicTicker, PrintfTickFormatter, Slider,
   Select, CheckboxGroup, CustomJS, CDSView, BooleanFilter, Div, Button
)
from bokeh.palettes import Viridis256
from bokeh.models.widgets import DataTable, TableColumn
 
 
output_notebook()
 
 
np.random.seed(42)
N = 300
data = pd.DataFrame({
   "temp_c": 20 + 5 * np.random.randn(N),
   "pressure_kpa": 101 + 3 * np.random.randn(N),
   "humidity_pct": 40 + 15 * np.random.randn(N),
   "sensor_id": np.random.choice(["A1","A2","B7","C3"], size=N),
   "timestep": np.arange(N)
})
 
 
source_main = ColumnDataSource(data)
 
 
p_scatter = figure(title="Temperature vs Pressure", width=400, height=300,
                  x_axis_label="Temperature (°C)", y_axis_label="Pressure (kPa)",
                  tools="pan,wheel_zoom,reset")
 
 
scat = p_scatter.circle(x="temp_c", y="pressure_kpa", size=8, fill_alpha=0.6,
                       fill_color="orange", line_color="black", source=source_main,
                       legend_label="Sensor Readings")
 
 
hover = HoverTool(tooltips=[
   ("Temp (°C)", "@temp_c{0.0}"), ("Pressure", "@pressure_kpa{0.0} kPa"),
   ("Humidity", "@humidity_pct{0.0}%"), ("Sensor", "@sensor_id"),
   ("Timestep", "@timestep")], renderers=[scat])
p_scatter.add_tools(hover)
p_scatter.legend.location = "top_left"
show(p_scatter)

Этот фрагмент создаёт синтетический набор данных и начальный scatter-плот температуры против давления с подсказками. ColumnDataSource — центральный объект данных в Bokeh, который связывает графики и виджеты.

Связанная выборка: влажность vs температура

Добавим второй график, связанный с тем же ColumnDataSource, чтобы инструменты выбора подсвечивали соответствующие точки на всех графиках.

p_humidity = figure(title="Humidity vs Temperature (Linked Selection)", width=400, height=300,
                   x_axis_label="Temperature (°C)", y_axis_label="Humidity (%)",
                   tools="pan,wheel_zoom,reset,box_select,lasso_select,tap")
 
 
r2 = p_humidity.square(x="temp_c", y="humidity_pct", size=8, fill_alpha=0.6,
                      fill_color="navy", line_color="white", source=source_main)
 
 
p_humidity.add_tools(HoverTool(tooltips=[
   ("Temp (°C)", "@temp_c{0.0}"), ("Humidity", "@humidity_pct{0.0}%"),
   ("Sensor", "@sensor_id")], renderers=[r2]))
 
 
layout_linked = row(p_scatter, p_humidity)
show(layout_linked)

Связанная выборка помогает исследовать взаимосвязи между разными переменными: выделение на одном графике автоматически появится на другом.

Непрерывная цветовая шкала

С помощью LinearColorMapper и ColorBar можно закодировать непрерывную переменную (влажность) в цветовой градиент.

color_mapper = LinearColorMapper(palette=Viridis256, low=data["humidity_pct"].min(),
                                high=data["humidity_pct"].max())
 
 
p_color = figure(title="Pressure vs Humidity (Colored by Humidity)", width=500, height=350,
                x_axis_label="Pressure (kPa)", y_axis_label="Humidity (%)",
                tools="pan,wheel_zoom,reset,box_select,lasso_select")
 
 
r3 = p_color.circle(x="pressure_kpa", y="humidity_pct", size=8, fill_alpha=0.8,
                   line_color=None, color={"field": "humidity_pct", "transform": color_mapper},
                   source=source_main)
 
 
color_bar = ColorBar(color_mapper=color_mapper, ticker=BasicTicker(desired_num_ticks=5),
                    formatter=PrintfTickFormatter(format="%4.1f%%"), label_standoff=8,
                    border_line_color=None, location=(0,0), title="Humidity %")
 
 
p_color.add_layout(color_bar, "right")
show(p_color)

Цветовая шкала и цветовая палитра делают визуализацию непрерывных величин более доступной для восприятия.

Интерактивные фильтры и таблица данных

Добавим виджеты Select, Slider и CheckboxGroup для фильтрации представления и выбора колонок для таблицы. BooleanFilter и CDSView применяются на стороне клиента.

sensor_options = sorted(data["sensor_id"].unique().tolist())
sensor_select = Select(title="Sensor ID Filter", value=sensor_options[0], options=sensor_options)
temp_slider = Slider(title="Max Temperature (°C)",
                    start=float(data["temp_c"].min()),
                    end=float(data["temp_c"].max()), step=0.5,
                    value=float(data["temp_c"].max()))
 
 
columns_available = ["temp_c", "pressure_kpa", "humidity_pct", "sensor_id", "timestep"]
checkbox_group = CheckboxGroup(labels=columns_available,
                              active=list(range(len(columns_available))))
 
 
def filter_mask(sensor_val, max_temp):
   return [(s == sensor_val) and (t <= max_temp)
           for s, t in zip(data["sensor_id"], data["temp_c"])]
 
 
bool_filter = BooleanFilter(filter_mask(sensor_select.value, temp_slider.value))
view = CDSView(filter=bool_filter)
 
 
p_filtered = figure(title="Filtered: Temp vs Pressure", width=400, height=300,
                   x_axis_label="Temp (°C)", y_axis_label="Pressure (kPa)",
                   tools="pan,wheel_zoom,reset,box_select,lasso_select")
 
 
r_filtered = p_filtered.circle(x="temp_c", y="pressure_kpa", size=8, fill_alpha=0.7,
                              fill_color="firebrick", line_color="white",
                              source=source_main, view=view)
 
 
p_filtered.add_tools(HoverTool(tooltips=[
   ("Temp", "@temp_c{0.0}"), ("Pressure", "@pressure_kpa{0.0}"),
   ("Humidity", "@humidity_pct{0.0}%"), ("Sensor", "@sensor_id")],
   renderers=[r_filtered]))
 
 
def make_table_src(cols):
   return ColumnDataSource(data[cols])
 
 
table_src = make_table_src(columns_available)
table_columns = [TableColumn(field=c, title=c) for c in columns_available]
table_widget = DataTable(source=table_src, columns=table_columns, width=500, height=200)
 
 
def update_filters(attr, old, new):
   bool_filter.booleans = filter_mask(sensor_select.value, temp_slider.value)
 
 
def update_table(attr, old, new):
   active_cols = [columns_available[i] for i in checkbox_group.active]
   new_src = make_table_src(active_cols)
   table_widget.source.data = new_src.data
   table_widget.columns = [TableColumn(field=c, title=c) for c in active_cols]
 
 
sensor_select.on_change("value", update_filters)
temp_slider.on_change("value", update_filters)
checkbox_group.on_change("active", update_table)
 
 
dashboard_controls = column(Div(text="<b>Interactive Filters</b>"), sensor_select,
                            temp_slider, Div(text="<b>Columns in Table</b>"), checkbox_group)
dashboard_layout = row(column(p_filtered, table_widget), dashboard_controls)
show(dashboard_layout)

Такие виджеты позволяют быстро исследовать подмножества данных и настраивать отображаемые столбцы таблицы.

Клиентская логика через CustomJS

CustomJS даёт возможность запускать логику полностью в браузере без обращения к Python-серверу. Пример ниже увеличивает маркеры синусоиды по клику кнопки.

mini_source = ColumnDataSource({
   "x": np.linspace(0, 2*np.pi, 80),
   "y": np.sin(np.linspace(0, 2*np.pi, 80))
})
 
 
p_wave = figure(title="Sine Wave (CustomJS: Enlarge points)", width=400, height=250,
               tools="pan,wheel_zoom,reset")
 
 
wave_render = p_wave.circle(x="x", y="y", size=6, fill_alpha=0.8,
                           fill_color="green", line_color="black", source=mini_source)
 
 
js_callback = CustomJS(args=dict(r=wave_render),
                      code="const new_size = r.glyph.size.value + 2; r.glyph.size = new_size;")
 
 
grow_button = Button(label="Enlarge points (CustomJS)", button_type="success")
grow_button.js_on_click(js_callback)
show(column(p_wave, grow_button))

Этот паттерн показывает, как легко добавить мгновенную обратную связь на стороне клиента.

Симуляция стриминга данных

Bokeh поддерживает потоковую подачу новых данных в графики. Ниже симулируется поток значений сенсора и отрисовка их как линии и точек.

stream_source = ColumnDataSource({"t": [], "val": []})
 
 
p_stream = figure(title="Streaming Sensor Value", width=500, height=250,
                 x_axis_label="timestep", y_axis_label="value",
                 tools="pan,wheel_zoom,reset")
 
 
p_stream.line(x="t", y="val", source=stream_source, line_width=3, line_alpha=0.8)
p_stream.circle(x="t", y="val", source=stream_source, size=6, fill_color="red")
show(p_stream)
 
 
for t in range(10):
   new_point = {"t": [t], "val": [np.sin(t/2) + 0.2*np.random.randn()]}
   stream_source.stream(new_point, rollover=200)
 
 
show(p_stream)

Стриминговые обновления демонстрируют, как Bokeh держит визуализации актуальными при поступлении новых данных.

Результат

Комбинация ColumnDataSource, связанных графиков, цветовой шкалы, интерактивных виджетов, CustomJS и стриминга позволяет создать отзывчивый дашборд в браузере. Такое сочетание Python-аналитики и клиентской реактивности обеспечивает гибкую и производительную интерактивную визуализацию.

Для полного кода и ноутбуков обратитесь к исходному репозиторию или материалам, упомянутым в руководстве.

🇬🇧

Switch Language

Read this article in English

Switch to English