←back to Blog

How to Build a Fully Interactive, Real-Time Visualization Dashboard Using Bokeh and Custom JavaScript?

«`html

Understanding the Target Audience

The target audience for the tutorial on building a fully interactive, real-time visualization dashboard using Bokeh and Custom JavaScript primarily consists of data analysts, data scientists, and business intelligence professionals. These individuals are typically engaged in roles that require them to visualize complex datasets and derive actionable insights from data.

Pain Points

  • Difficulty in creating interactive and real-time visualizations that effectively communicate data insights.
  • Challenges in integrating Python-based analytics with responsive web technologies like JavaScript.
  • Need for tools that allow for dynamic data filtering and exploration without extensive programming knowledge.

Goals

  • To develop skills in using Bokeh for creating visually appealing and interactive dashboards.
  • To learn how to implement real-time data visualization techniques that enhance decision-making processes.
  • To gain proficiency in integrating Custom JavaScript for enhanced interactivity in data visualizations.

Interests

  • Data visualization best practices and techniques.
  • Emerging technologies in data analytics and business intelligence.
  • Hands-on tutorials and practical applications of data visualization tools.

Communication Preferences

The audience prefers clear, concise, and practical content that includes step-by-step instructions, code snippets, and real-world examples. They appreciate visual aids and interactive elements that enhance their learning experience.

Building a Fully Interactive, Real-Time Visualization Dashboard Using Bokeh and Custom JavaScript

In this tutorial, we create a fully interactive, visually compelling data visualization dashboard using Bokeh. We start by turning raw data into insightful plots, then enhance them with features such as linked brushing, color gradients, and real-time filters powered by dropdowns and sliders. As we progress, we bring our dashboard to life with Custom JavaScript (CustomJS) interactivity, enabling instant browser-side responses without a single Python callback. By blending the best of Python’s analytical strength with JavaScript’s responsiveness, we build a seamless, dynamic dashboard experience that redefines how we visualize and interact with data.

Setting Up the Environment

To begin, we need to set up our environment and import all the necessary libraries:

!pip install bokeh pandas numpy scipy -q

Creating a Synthetic Dataset

We create a synthetic dataset to visualize temperature against pressure using a simple scatter plot with hover functionality. This helps us establish a foundation for our interactive dashboard.

import numpy as np
import pandas as pd
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, HoverTool

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)

Linked Selection for Enhanced Analysis

We extend our visualization by adding another plot that links humidity and temperature through shared data. We use linked brushing so that selections in one plot automatically reflect in the other, helping us analyze relationships across multiple variables simultaneously.

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)

Color Mapping for Enhanced Visualization

We enhance our visualization by introducing a continuous color mapping feature to represent humidity levels. By adding a color bar and gradient, we make our chart more informative and intuitive, allowing us to interpret variations visually.

from bokeh.models import LinearColorMapper, ColorBar, BasicTicker, PrintfTickFormatter

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)

Dynamic Filtering with Interactive Widgets

We introduce interactivity through widgets such as dropdowns, sliders, and checkboxes. We dynamically filter data and update tables in real time, enabling us to easily explore different subsets and attributes of the dataset.

from bokeh.models import Select, Slider, CheckboxGroup, BooleanFilter, CDSView, DataTable, TableColumn

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="Interactive Filters"), sensor_select,
                            temp_slider, Div(text="Columns in Table"), checkbox_group)
dashboard_layout = row(column(p_filtered, table_widget), dashboard_controls)
show(dashboard_layout)

Client-Side Interactivity with CustomJS

We implement a JavaScript-based interaction using Bokeh’s CustomJS. We create a sine wave visualization and allow users to enlarge the plot markers with a button click, demonstrating client-side control without any Python callbacks.

from bokeh.models import 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))

Simulating Real-Time Data Streaming

We simulate a live data stream by continuously adding new data points to our plot. We watch the visualization update dynamically, showcasing how Bokeh can handle real-time data and provide instant visual feedback.

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)

Conclusion

In conclusion, we create a fully functional, real-time, and browser-interactive dashboard that showcases the full potential of Bokeh. We learn how to visualize multiple dimensions of data, dynamically filter and update visuals, and even harness JavaScript integration to make instant, client-side updates directly within the browser. This hands-on experience shows us how Bokeh effortlessly merges Python and JavaScript, empowering us to design dashboards that are not just interactive but intelligent, responsive, and production-ready.

Check out the FULL CODES here. Feel free to check out our GitHub Page for Tutorials, Codes, and Notebooks. Also, feel free to follow us on Twitter and don’t forget to join our 100k+ ML SubReddit and Subscribe to our Newsletter. Wait! are you on Telegram? Now you can join us on Telegram as well.

«`