第十章:实战案例
前置知识:本章综合运用了以下知识点:ColumnDataSource 与选择状态(第二章 2.2-2.6)、基础图表(第三章)、布局系统(第五章)、交互功能(第六章)、Bokeh Server(第七章)。
10.1 案例一:股票分析仪表板
案例说明:本案例构建一个股票分析仪表板,综合演示以下知识点:
- K 线图与成交量图:使用
segment()+vbar()组合绘制(第三章 3.3 柱状图的扩展应用) - 时间轴配置:
x_axis_type="datetime"(第二章 2.3 Figure 配置) - 共享 X 轴:K 线图和成交量图通过
x_range=p_candle.x_range联动(第六章 6.6 链接轴) - 悬停工具:
HoverTool的日期格式化@date{%F}(第六章 6.1) - Bokeh Server:使用
curdoc().add_root()添加布局(第七章 7.2)
运行方式:保存为 stock_dashboard.py,执行 bokeh serve stock_dashboard.py。
# stock_dashboard.py
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.models import (
ColumnDataSource, Select, DateRangeSlider, HoverTool,
CrosshairTool, Span, Label, NumeralTickFormatter
)
from bokeh.layouts import column, row
from bokeh.palettes import Category10
import pandas as pd
import numpy as np
# 模拟股票数据
np.random.seed(42)
dates = pd.date_range('2022-01-01', '2023-12-31', freq='D')
n = len(dates)
base_price = 100
prices = base_price + np.random.randn(n).cumsum()
volumes = np.random.randint(1000000, 10000000, n)
df = pd.DataFrame({
'date': dates,
'open': prices + np.random.randn(n) * 2,
'high': prices + np.abs(np.random.randn(n)) * 3,
'low': prices - np.abs(np.random.randn(n)) * 3,
'close': prices + np.random.randn(n) * 2,
'volume': volumes,
})
df['ma5'] = df['close'].rolling(5).mean()
df['ma20'] = df['close'].rolling(20).mean()
source = ColumnDataSource(df)
# K线图
p_candle = figure(x_axis_type="datetime", width=900, height=400,
title="K线图", tools="pan,wheel_zoom,box_zoom,reset")
# 绘制K线
inc = df.close > df.open
dec = df.open > df.close
p_candle.segment('date', 'high', 'date', 'low', source=source, color="black")
p_candle.vbar('date', width=0.5, bottom='open', top='close', source=source,
fill_color="red", line_color="black") # 简化版,实际需区分涨跌
# 均线
p_candle.line('date', 'ma5', source=source, line_color="blue", line_width=1, legend_label="MA5")
p_candle.line('date', 'ma20', source=source, line_color="orange", line_width=1, legend_label="MA20")
# 成交量图
p_vol = figure(x_axis_type="datetime", width=900, height=150,
title="成交量", x_range=p_candle.x_range)
p_vol.vbar('date', top='volume', source=source, width=0.5, fill_color="skyblue")
# 悬停工具
hover = HoverTool(tooltips=[
("日期", "@date{%F}"),
("开盘", "@open{0.00}"),
("最高", "@high{0.00}"),
("最低", "@low{0.00}"),
("收盘", "@close{0.00}"),
("成交量", "@volume{0,0}"),
], formatters={'@date': 'datetime'})
p_candle.add_tools(hover)
# 十字光标
crosshair = CrosshairTool(dimensions="both")
p_candle.add_tools(crosshair)
# 布局
layout = column(p_candle, p_vol)
curdoc().add_root(layout)
curdoc().title = "股票分析"
10.2 案例二:实时服务器监控
案例说明:本案例构建一个实时服务器监控面板,综合演示以下知识点:
- 实时数据更新:
curdoc().add_periodic_callback()定时刷新(第七章 7.6 定时回调) - 滚动时序图:通过更新
source.data实现动态折线图(第二章 2.2 ColumnDataSource 数据更新) - 状态卡片:使用
Div组件动态更新 HTML 内容(第七章 7.3 Widget) - 网格布局:
gridplot()排列多个监控图表(第五章 5.2 布局系统) - 范围控制:
p.x_range.start/end实现时间轴自动滚动
运行方式:保存为 monitor.py,执行 bokeh serve monitor.py。
# monitor.py
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, Div
from bokeh.layouts import column, row, gridplot
from collections import deque
import random
import time
# 配置
MAX_POINTS = 100
UPDATE_INTERVAL = 500 # ms
# 数据存储
times = deque(maxlen=MAX_POINTS)
cpu_data = deque(maxlen=MAX_POINTS)
mem_data = deque(maxlen=MAX_POINTS)
net_data = deque(maxlen=MAX_POINTS)
# 数据源
cpu_source = ColumnDataSource(data={'x': [], 'y': []})
mem_source = ColumnDataSource(data={'x': [], 'y': []})
net_source = ColumnDataSource(data={'x': [], 'y': []})
# 状态卡片
cpu_card = Div(text="<div style='background:#2196F3;padding:20px;border-radius:10px;width:150px;'><h3 style='color:white;margin:0;'>CPU</h3><h1 style='color:white;margin:5px 0;'>0%</h1></div>")
mem_card = Div(text="<div style='background:#4CAF50;padding:20px;border-radius:10px;width:150px;'><h3 style='color:white;margin:0;'>内存</h3><h1 style='color:white;margin:5px 0;'>0%</h1></div>")
net_card = Div(text="<div style='background:#FF9800;padding:20px;border-radius:10px;width:150px;'><h3 style='color:white;margin:0;'>网络</h3><h1 style='color:white;margin:5px 0;'>0 MB/s</h1></div>")
# 图表
cpu_plot = figure(width=400, height=250, title="CPU使用率", tools="pan,wheel_zoom")
cpu_plot.line('x', 'y', source=cpu_source, line_width=2, line_color="#2196F3")
cpu_plot.y_range.start = 0
cpu_plot.y_range.end = 100
mem_plot = figure(width=400, height=250, title="内存使用率", tools="pan,wheel_zoom")
mem_plot.line('x', 'y', source=mem_source, line_width=2, line_color="#4CAF50")
mem_plot.y_range.start = 0
mem_plot.y_range.end = 100
net_plot = figure(width=400, height=250, title="网络流量", tools="pan,wheel_zoom")
net_plot.line('x', 'y', source=net_source, line_width=2, line_color="#FF9800")
# 更新函数
start_time = time.time()
def update():
t = time.time() - start_time
# 模拟数据
cpu = random.uniform(20, 80)
mem = random.uniform(40, 70)
net = random.uniform(1, 50)
# 更新数据
times.append(t)
cpu_data.append(cpu)
mem_data.append(mem)
net_data.append(net)
# 更新数据源
cpu_source.data = {'x': list(times), 'y': list(cpu_data)}
mem_source.data = {'x': list(times), 'y': list(mem_data)}
net_source.data = {'x': list(times), 'y': list(net_data)}
# 更新卡片
cpu_card.text = f"<div style='background:#2196F3;padding:20px;border-radius:10px;width:150px;'><h3 style='color:white;margin:0;'>CPU</h3><h1 style='color:white;margin:5px 0;'>{cpu:.1f}%</h1></div>"
mem_card.text = f"<div style='background:#4CAF50;padding:20px;border-radius:10px;width:150px;'><h3 style='color:white;margin:0;'>内存</h3><h1 style='color:white;margin:5px 0;'>{mem:.1f}%</h1></div>"
net_card.text = f"<div style='background:#FF9800;padding:20px;border-radius:10px;width:150px;'><h3 style='color:white;margin:0;'>网络</h3><h1 style='color:white;margin:5px 0;'>{net:.1f} MB/s</h1></div>"
# 滚动x轴
if t > MAX_POINTS:
for p in [cpu_plot, mem_plot, net_plot]:
p.x_range.start = t - MAX_POINTS
p.x_range.end = t
# 布局
layout = column(
row(cpu_card, mem_card, net_card),
gridplot([[cpu_plot, mem_plot], [net_plot, None]])
)
curdoc().add_root(layout)
curdoc().title = "服务器监控"
curdoc().add_periodic_callback(update, UPDATE_INTERVAL)
10.3 案例三:地理数据可视化
案例说明:本案例展示地理数据可视化,综合演示以下知识点:
- 瓦片地图:使用
add_tile()加载底图(第三章 3.11 地理可视化) - 坐标转换:经纬度转 Web 墨卡托投影
- 气泡大小映射:先把人口缩放为适合显示的
size列,再映射到点大小 - 悬停工具:显示城市名、人口、GDP 等信息(第六章 6.1)
运行方式:保存为 geo_map.py,执行 bokeh serve geo_map.py --show。
运行方式说明:本例使用
curdoc(),属于 Bokeh Server 应用。请用bokeh serve运行,而不是直接python geo_map.py或show()导出静态 HTML。
# geo_map.py
from bokeh.io import curdoc
from bokeh.models import ColumnDataSource, HoverTool
from bokeh.plotting import figure
import pandas as pd
import numpy as np
import xyzservices.providers as xyz
# 经纬度转 Web 墨卡托
def lonlat_to_web_mercator(lon, lat):
k = 6378137
x = lon * (k * np.pi / 180.0)
y = np.log(np.tan((90 + lat) * np.pi / 360.0)) * k
return x, y
# 模拟城市数据
cities = pd.DataFrame({
'name': ['北京', '上海', '广州', '深圳', '成都', '杭州', '武汉', '西安'],
'lon': [116.4, 121.5, 113.3, 114.1, 104.1, 120.2, 114.3, 108.9],
'lat': [39.9, 31.2, 23.1, 22.5, 30.7, 30.3, 30.6, 34.3],
'population': [2154, 2424, 1530, 1303, 1634, 1036, 1121, 1295],
'gdp': [36102, 38701, 25019, 27670, 17013, 15373, 15616, 10020],
})
# 转换坐标
cities['x'], cities['y'] = lonlat_to_web_mercator(cities['lon'], cities['lat'])
# 将人口缩放为更合理的气泡大小(像素)
cities['size'] = cities['population'] / 120
source = ColumnDataSource(cities)
p = figure(
x_axis_type="mercator",
y_axis_type="mercator",
width=800,
height=600,
title="中国主要城市分布",
)
# Bokeh 3.x 推荐直接给 add_tile() 传入 xyzservices provider
p.add_tile(xyz.CartoDB.Positron)
# 绘制城市点
p.circle(
'x',
'y',
source=source,
size='size',
fill_color="red",
fill_alpha=0.6,
line_color="black",
)
# 悬停
hover = HoverTool(tooltips=[
("城市", "@name"),
("人口", "@population{0,0}万"),
("GDP", "@gdp{0,0}亿"),
])
p.add_tools(hover)
# 设置地图范围(中国区域)
p.x_range.start = 10000000
p.x_range.end = 15000000
p.y_range.start = 2000000
p.y_range.end = 7000000
curdoc().add_root(p)
curdoc().title = "城市分布图"
10.4 本章小结
本章通过三个综合案例,把前面章节中的核心能力串了起来:
- 股票分析仪表板:强调时间序列、共享坐标轴、悬停提示和多图组合,可与第三章、第五章、第六章对照阅读。
- 实时服务器监控:强调
ColumnDataSource更新、curdoc().add_periodic_callback()和 Bokeh Server 运行模型,建议结合第二章和第七章一起理解。 - 地理数据可视化:强调 Web Mercator 坐标、
add_tile()、悬停提示和点位映射,可回看第三章中图表选择与第八章中的嵌入/分享方式。
如果你准备把案例改造成自己的项目,推荐按下面顺序进行:
- 先明确数据结构,确保字段命名和类型稳定。
- 再决定图表类型、布局方式和交互模式。
- 如果需要 Python 回调、实时更新或多控件协同,再切换到 Bokeh Server。
- 如果数据量增大,再参考第九章做降采样、增量更新或服务端处理。
10.5 常见坑
常见坑 1:把案例直接当成生产代码
本章案例以“教学演示”为主,很多数据是模拟的,部分逻辑也做了简化。
例如股票案例中的 K 线配色、城市案例中的气泡大小缩放、监控案例中的随机数据,都需要按你的真实业务进一步改造。
常见坑 2:忽略运行方式
本章三个案例都使用了 curdoc(),属于 Bokeh Server 应用。
如果直接运行 python xxx.py,或者只调用 show() / save(),Python 回调和定时更新都不会生效。运行方式请统一使用:
bokeh serve stock_dashboard.py --showbokeh serve monitor.py --showbokeh serve geo_map.py --show
如果你只需要独立 HTML,请回看第八章中的 standalone 输出方式,并把 Python 回调改写为 CustomJS。
常见坑 3:案例之间重复造轮子
这三个案例其实复用了很多共同模式:
- 共享
ColumnDataSource - 多图组合布局
- HoverTool / CrosshairTool 等交互工具
- Bokeh Server 文档模型
建议你在自己的项目里提炼公共函数,而不是每个案例都从头复制粘贴。
例如可以统一封装数据加载、样式配置、HoverTool 模板和布局函数,这样后续维护会更轻松。
常见坑 4:一开始就追求“大全套”
实战项目里最常见的问题不是“功能太少”,而是“第一次就把筛选、联动、导出、部署、性能优化全部堆上去”。
更稳妥的做法是:
- 先做一个最小可运行版本。
- 确认数据和图表表达正确。
- 再逐步增加交互、布局、输出和性能优化。
这样也更符合本教程前九章的学习路径。
延伸阅读:建议先完成前置章节的学习再阅读本章案例。如遇到问题可参考故障排查。如果你准备把案例继续扩展为完整应用,建议顺序阅读第六章:交互功能场景实战 → 第七章:Bokeh Server 进阶 → 第八章:输出选项 → 第九章:性能优化。