<RETURN_TO_BASE

Real-Time Interactive Dashboards with Bokeh and CustomJS

'Step-by-step guide to create a dynamic Bokeh dashboard with linked plots, interactive filters, color mapping and client-side CustomJS interactivity.'

Setup and imports

Start by installing and importing the libraries required for building interactive Bokeh visualizations and handling data.

!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)

This creates a synthetic dataset and an initial scatter plot of temperature vs pressure with hover tooltips. The ColumnDataSource is the central data object that Bokeh uses to drive all linked plots and widgets.

Linked brushing: humidity vs temperature

Add a second plot linked to the same ColumnDataSource so that selection tools highlight corresponding points across plots.

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)

Linked brushing makes it easy to inspect multi-dimensional relationships: select points on one plot and see the same points highlighted on the other.

Continuous color mapping

Use a LinearColorMapper and a color bar to encode a continuous variable (humidity) as a color gradient on another scatter plot.

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)

A color-encoded plot combined with a color bar improves readability for continuous features and helps spot patterns at a glance.

Interactive filters and data table

Add widgets like Select, Slider and CheckboxGroup to filter the view and change which table columns are shown. BooleanFilter and CDSView let you apply client-side filtering efficiently.

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)

Widgets enable quick exploration of subsets of the data and let users customize which attributes appear in the data table.

Client-side interactivity with CustomJS

CustomJS allows interactions to run entirely in the browser without round trips to the Python server. Below is an example that enlarges sine wave point markers using a button.

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))

This demonstrates how small UI controls wired to CustomJS can create immediate visual feedback on the client without invoking Python callbacks.

Simulated streaming data

Bokeh supports streaming new data into plots. Here we simulate streaming sensor values and render them as a line and circles.

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)

Streaming updates show how Bokeh can handle live or incremental data flows to keep visualizations current.

What you get

Combining ColumnDataSource-driven plots, linked brushing, continuous color mapping, interactive widgets, CustomJS callbacks, and streaming gives you a responsive, browser-powered dashboard. This setup blends Python-based data processing with JavaScript-driven UI responsiveness to produce a production-ready interactive visualization experience.

For full code samples and notebooks, check the original source or the associated project repository referenced in the tutorial.

🇷🇺

Сменить язык

Читать эту статью на русском

Переключить на Русский