Интерактивный дашборд в реальном времени с Bokeh и CustomJS
'Гайд по созданию интерактивного дашборда на Bokeh с клиентскими CustomJS-колбэками, фильтрами, цветовой картой и стримингом данных.'
Установка и импорт
Сначала установим и импортируем библиотеки, необходимые для создания интерактивных визуализаций в Bokeh и работы с данными.
!pip install bokeh pandas numpy scipy -qimport 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