示例图表
这是一个通过自定义模板嵌入的 Bokeh 图表。
{{ div }}Bokeh 是一个针对现代 Web 浏览器的交互式可视化库,专注于为大型数据集提供优雅、简洁的呈现。与 Matplotlib 和 Seaborn 等传统静态可视化库不同,Bokeh 生成的是在浏览器中渲染的交互式图表,具有缩放、平移、悬停等丰富的互动功能。
Bokeh 的核心理念是创建面向 Web 的可视化工具,它使用现代 Web 技术(如 HTML5 Canvas 和 WebGL)实现高性能图形渲染,无需依赖 JavaScript 编程即可在 Python 中构建复杂的可视化项目。
特性 | Bokeh | Matplotlib | Plotly | D3.js |
---|---|---|---|---|
交互性 | 原生支持 | 有限 | 原生支持 | 完全支持 |
开发语言 | Python | Python | Python/R/JS | JavaScript |
输出格式 | HTML/服务器 | 图片/PDF | HTML/服务器 | HTML |
大数据支持 | 良好 | 有限 | 良好 | 有限 |
学习曲线 | 中等 | 平缓 | 中等 | 陡峭 |
编程模型 | 声明式/命令式 | 命令式 | 声明式/命令式 | 声明式 |
独立性 | 可独立使用 | 可独立使用 | 依赖云服务(免费版) | 需要网页环境 |
Bokeh 特别适合以下应用场景:
# 使用 pip 安装
pip install bokeh
# 或使用 conda 安装
conda install bokeh -c conda-forge
Bokeh 的核心依赖包括:
from bokeh.plotting import figure, show
from bokeh.io import output_notebook
# 创建一个简单图表验证安装
plot = figure(width=400, height=400, title="Bokeh 安装验证")
plot.circle([1, 2, 3, 4, 5], [6, 7, 8, 9, 10], size=10)
# 在笔记本中展示图表
output_notebook()
show(plot)
# 或将图表保存为HTML文件
from bokeh.io import output_file
output_file("bokeh_test.html")
show(plot)
对于 Bokeh 开发,推荐以下环境设置:
# Jupyter Notebook 中配置
from bokeh.io import output_notebook, set_curdoc
from bokeh.themes import Theme
# 启用笔记本输出
output_notebook()
# 设置主题(可选)
theme = Theme(json={
'attrs': {
'Figure': {
'background_fill_color': '#f5f5f5',
'border_fill_color': '#ffffff',
'outline_line_color': '#444444',
},
'Axis': {
'axis_line_color': '#444444',
'major_tick_line_color': '#444444',
'minor_tick_line_color': '#444444',
},
'Grid': {
'grid_line_color': '#dddddd',
}
}
})
set_curdoc(theme=theme)
Bokeh 的绘图架构包含以下核心概念:
from bokeh.plotting import figure, show
from bokeh.io import output_file
# 准备数据
x = [1, 2, 3, 4, 5]
y = [6, 7, 2, 4, 5]
# 创建图表
p = figure(title="基本散点图",
x_axis_label="X轴",
y_axis_label="Y轴",
width=600, height=400)
# 添加圆形图元
p.circle(x, y, size=10, color="navy", alpha=0.5)
# 输出为HTML文件并显示
output_file("scatter.html")
show(p)
from bokeh.plotting import figure, show
from bokeh.io import output_file
import numpy as np
# 准备数据
x = np.linspace(0, 10, 100)
y = np.sin(x)
# 创建图表
p = figure(title="基本线图",
x_axis_label="X轴",
y_axis_label="Y轴",
width=600, height=400)
# 添加线条图元
p.line(x, y, line_width=2, color="coral")
# 添加圆形标记
p.circle(x, y, size=6, color="coral", alpha=0.3)
# 输出为HTML文件并显示
output_file("line.html")
show(p)
from bokeh.plotting import figure, show
from bokeh.io import output_file
# 准备数据
fruits = ['苹果', '橙子', '香蕉', '梨', '葡萄']
counts = [5, 3, 4, 2, 4]
# 创建图表
p = figure(x_range=fruits,
title="水果数量统计",
x_axis_label="水果",
y_axis_label="数量",
width=600, height=400,
toolbar_location="right")
# 添加柱状图
p.vbar(x=fruits, top=counts, width=0.5, color="green", alpha=0.6)
# 设置其他属性
p.xgrid.grid_line_color = None
p.y_range.start = 0
# 输出为HTML文件并显示
output_file("bar.html")
show(p)
Bokeh 提供丰富的图形属性设置选项:
from bokeh.plotting import figure, show
from bokeh.io import output_file
# 准备数据
x = [1, 2, 3, 4, 5]
y = [6, 7, 2, 4, 5]
# 创建图表
p = figure(title="图形属性设置示例")
# 添加圆形图元,设置属性
p.circle(x, y,
size=20, # 大小
color="navy", # 填充颜色
alpha=0.5, # 透明度
line_color="orange", # 边框颜色
line_width=2, # 边框宽度
line_dash="dashed", # 边框样式:dashed, dotted, solid 等
legend_label="数据系列" # 图例标签
)
# 设置图表属性
p.title.text_font_size = "20px" # 标题字体大小
p.title.text_font_style = "italic" # 标题字体样式
p.title.align = "center" # 标题对齐方式
# 设置坐标轴属性
p.xaxis.axis_label = "X轴"
p.yaxis.axis_label = "Y轴"
p.axis.axis_label_text_font_style = "bold"
p.axis.major_label_text_font_size = "14px"
# 设置网格线属性
p.grid.grid_line_color = "gray"
p.grid.grid_line_alpha = 0.3
p.grid.grid_line_dash = [6, 4]
# 设置图例属性
p.legend.location = "top_left"
p.legend.title = "图例标题"
p.legend.title_text_font_style = "bold"
p.legend.border_line_color = "black"
p.legend.background_fill_alpha = 0.7
# 输出为HTML文件并显示
output_file("styled_plot.html")
show(p)
Bokeh 允许在同一图表中组合多种图形:
from bokeh.plotting import figure, show
from bokeh.io import output_file
import numpy as np
# 准备数据
x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.cos(x)
y3 = np.tan(x/3)
# 创建图表
p = figure(title="多图形组合示例", width=700, height=400)
# 添加多个图形
p.line(x, y1, color="red", legend_label="sin(x)", line_width=2)
p.circle(x, y1, color="red", size=6, alpha=0.3)
p.line(x, y2, color="blue", legend_label="cos(x)", line_width=2)
p.square(x, y2, color="blue", size=6, alpha=0.3)
# 多图形可以通过 visible 属性控制显示和隐藏
tangent = p.line(x, y3, color="green", legend_label="tan(x/3)",
line_width=2, visible=False)
p.triangle(x, y3, color="green", size=6, alpha=0.3, visible=False)
# 设置图例为点击切换显示/隐藏
p.legend.click_policy = "hide"
# 输出为HTML文件并显示
output_file("combined_plot.html")
show(p)
Bokeh 提供多种内置交互工具:
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.models import HoverTool, BoxZoomTool, ResetTool, SaveTool, PanTool
# 准备数据
x = [1, 2, 3, 4, 5]
y = [6, 7, 2, 4, 5]
# 创建图表,指定工具
p = figure(title="交互工具示例",
tools="pan,box_zoom,wheel_zoom,lasso_select,reset,save,hover",
tooltips=[("索引", "$index"), ("(x,y)", "($x, $y)")],
width=600, height=400)
# 添加圆形图元
p.circle(x, y, size=20, color="navy", alpha=0.5)
# 或者可以单独添加工具
hover = HoverTool(tooltips=[
("索引", "$index"),
("(x,y)", "($x, $y)"),
("描述", "@desc") # 如果数据中有desc列,则显示该列值
])
p.add_tools(hover)
# 输出为HTML文件并显示
output_file("interactive_tools.html")
show(p)
Bokeh 的 HoverTool 可以高度自定义:
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.models import ColumnDataSource, HoverTool
import pandas as pd
# 准备带有额外数据的数据源
source = ColumnDataSource(data=dict(
x=[1, 2, 3, 4, 5],
y=[6, 7, 2, 4, 5],
desc=['A', 'B', 'C', 'D', 'E'],
imgs=['image1.jpg', 'image2.jpg', 'image3.jpg', 'image4.jpg', 'image5.jpg'],
colors=['red', 'green', 'blue', 'orange', 'purple']
))
# 创建图表
p = figure(title="自定义悬停提示示例", width=600, height=400)
# 添加圆形图元,使用数据源
circles = p.circle('x', 'y', size=20, fill_color='colors', line_color='black',
source=source, alpha=0.6)
# 添加自定义悬停工具
hover = HoverTool(renderers=[circles], tooltips="""
@desc
坐标:
($x, $y)
""")
p.add_tools(hover)
# 输出为HTML文件并显示
output_file("custom_hover.html")
show(p)
Bokeh 支持选择交互和图表间的选择链接:
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.layouts import row
from bokeh.models import ColumnDataSource, TapTool, CustomJS
import numpy as np
# 准备数据源
source1 = ColumnDataSource(data=dict(
x=np.random.rand(20),
y=np.random.rand(20),
))
source2 = ColumnDataSource(data=dict(
x=np.random.rand(20),
y=np.random.rand(20),
))
# 创建第一个图表
p1 = figure(title="图表1 - 点击选择", width=400, height=400,
tools="tap,pan,wheel_zoom,reset")
p1.circle('x', 'y', source=source1, size=15, color="navy", alpha=0.5,
selection_color="firebrick", nonselection_alpha=0.1)
# 创建第二个图表
p2 = figure(title="图表2 - 联动高亮", width=400, height=400,
tools="tap,pan,wheel_zoom,reset")
p2.circle('x', 'y', source=source2, size=15, color="green", alpha=0.5,
selection_color="firebrick", nonselection_alpha=0.1)
# 创建选择联动的JavaScript回调
callback = CustomJS(args=dict(source1=source1, source2=source2), code="""
// 获取选中的索引
const selected_indices = source1.selected.indices;
// 更新第二个图表的选择
source2.selected.indices = selected_indices;
""")
# 为第一个图表的选择添加回调
source1.selected.js_on_change('indices', callback)
# 布局并展示图表
layout = row(p1, p2)
output_file("linked_selection.html")
show(layout)
Bokeh 提供了丰富的交互控件:
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.layouts import column, row
from bokeh.models import Slider, Button, ColumnDataSource, CustomJS
import numpy as np
# 准备初始数据
x = np.linspace(0, 10, 500)
y = np.sin(x)
source = ColumnDataSource(data=dict(x=x, y=y))
# 创建图表
p = figure(title="交互控件示例", width=800, height=400)
p.line('x', 'y', source=source, line_width=3, line_alpha=0.6, color="navy")
# 创建滑块控件
amplitude_slider = Slider(start=0.1, end=2, value=1, step=0.1, title="振幅")
frequency_slider = Slider(start=0.1, end=5, value=1, step=0.1, title="频率")
phase_slider = Slider(start=0, end=6.28, value=0, step=0.1, title="相位")
offset_slider = Slider(start=-2, end=2, value=0, step=0.1, title="偏移")
# 创建按钮
reset_button = Button(label="重置参数", button_type="success")
# 添加JavaScript回调来更新图表
callback = CustomJS(args=dict(source=source,
amp=amplitude_slider,
freq=frequency_slider,
phase=phase_slider,
offset=offset_slider), code="""
// 获取滑块当前值
const amp = amp.value;
const freq = freq.value;
const phase = phase.value;
const offset = offset.value;
// 重新计算数据
const x = source.data.x;
const y = new Array(x.length);
for (let i = 0; i < x.length; i++) {
y[i] = amp * Math.sin(freq * x[i] + phase) + offset;
}
// 更新数据源
source.data.y = y;
source.change.emit();
""")
# 为滑块添加回调
amplitude_slider.js_on_change('value', callback)
frequency_slider.js_on_change('value', callback)
phase_slider.js_on_change('value', callback)
offset_slider.js_on_change('value', callback)
# 添加重置按钮回调
reset_callback = CustomJS(args=dict(source=source,
amp=amplitude_slider,
freq=frequency_slider,
phase=phase_slider,
offset=offset_slider), code="""
// 重置滑块值
amp.value = 1;
freq.value = 1;
phase.value = 0;
offset.value = 0;
""")
reset_button.js_on_click(reset_callback)
# 布局控件和图表
layout = column(
p,
row(amplitude_slider, frequency_slider),
row(phase_slider, offset_slider),
reset_button
)
# 输出为HTML文件并显示
output_file("interactive_controls.html")
show(layout)
Bokeh 提供灵活的布局系统,包括行、列和网格:
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.layouts import column, row, grid
import numpy as np
# 创建四个简单图表
p1 = figure(width=300, height=300, title="图表1")
p1.circle(np.random.rand(10), np.random.rand(10), size=10, color="navy")
p2 = figure(width=300, height=300, title="图表2")
p2.line(np.arange(10), np.random.rand(10), line_width=2, color="coral")
p3 = figure(width=300, height=300, title="图表3")
p3.vbar(x=np.arange(5), top=np.random.rand(5)*10, width=0.5, color="green")
p4 = figure(width=300, height=300, title="图表4")
x, y = np.meshgrid(np.linspace(0, 1, 20), np.linspace(0, 1, 20))
z = np.sin(x*6) * np.cos(y*6)
p4.image(image=[z], x=0, y=0, dw=1, dh=1, palette="Spectral11")
# 行布局
row_layout = row(p1, p2)
# 列布局
col_layout = column(p1, p2)
# 嵌套布局
nested_layout = column(
row(p1, p2),
row(p3, p4)
)
# 网格布局
grid_layout = grid(
[p1, p2, p3, p4], # 包含的图表列表
ncols=2 # 列数
)
# 显示布局
output_file("grid_layout.html")
show(grid_layout)
Bokeh 支持创建标签页导航:
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.models.widgets import Tabs, Panel
from bokeh.layouts import column
import numpy as np
# 创建第一个面板内容
p1 = figure(width=600, height=400, title="散点图面板")
p1.circle(np.random.rand(50)*10, np.random.rand(50)*10,
size=10, color="navy", alpha=0.6)
# 创建第二个面板内容 - 折线图
x = np.linspace(0, 10, 100)
p2 = figure(width=600, height=400, title="折线图面板")
p2.line(x, np.sin(x), line_width=2, color="coral")
p2.line(x, np.cos(x), line_width=2, color="green")
# 创建第三个面板内容 - 柱状图
fruits = ['苹果', '橙子', '香蕉', '梨', '葡萄']
counts = [5, 3, 4, 2, 4]
p3 = figure(x_range=fruits, width=600, height=400, title="柱状图面板")
p3.vbar(x=fruits, top=counts, width=0.5, color="green", alpha=0.6)
# 创建面板对象
panel1 = Panel(child=p1, title="散点图")
panel2 = Panel(child=p2, title="折线图")
panel3 = Panel(child=p3, title="柱状图")
# 组合标签页
tabs = Tabs(tabs=[panel1, panel2, panel3])
# 输出为HTML文件并显示
output_file("tabs.html")
show(tabs)
Bokeh 可以嵌入到自定义 HTML 模板中:
from bokeh.plotting import figure
from bokeh.embed import components
from bokeh.io import output_file, save
from bokeh.resources import CDN
from jinja2 import Template
# 创建简单图表
p = figure(width=600, height=400, title="嵌入示例")
p.line([1, 2, 3, 4, 5], [6, 7, 2, 4, 5], line_width=2, color="navy")
# 生成组件
script, div = components(p)
# 创建自定义 HTML 模板
template = Template('''
自定义 Bokeh 模板
{{ resources }}
{{ script }}
自定义 Bokeh 可视化
通过自定义模板增强可视化体验
示例图表
这是一个通过自定义模板嵌入的 Bokeh 图表。
{{ div }}
''')
# 渲染模板
html = template.render(resources=CDN.render(), script=script, div=div)
# 保存为 HTML 文件
with open("custom_template.html", "w", encoding="utf-8") as f:
f.write(html)
print("自定义模板已保存为 custom_template.html")
Bokeh 服务器允许创建更复杂的交互式应用:
# 保存为 bokeh_server_app.py
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, Select, Slider
from bokeh.layouts import column, row
from bokeh.io import curdoc
import numpy as np
# 准备初始数据
x = np.linspace(0, 10, 500)
y = np.sin(x)
source = ColumnDataSource(data=dict(x=x, y=y))
# 创建图表
p = figure(title="Bokeh 服务器示例", width=800, height=400)
p.line('x', 'y', source=source, line_width=3, line_alpha=0.6, color="navy")
# 创建控制部件
function_select = Select(title="函数选择:",
value="sin",
options=["sin", "cos", "tan", "x^2"])
amplitude_slider = Slider(title="振幅", value=1.0, start=0.1, end=5.0, step=0.1)
frequency_slider = Slider(title="频率", value=1.0, start=0.1, end=5.0, step=0.1)
# 更新函数
def update_data(attr, old, new):
# 获取当前控件值
f = function_select.value
a = amplitude_slider.value
w = frequency_slider.value
# 更新数据
x = np.linspace(0, 10, 500)
if f == 'sin':
y = a * np.sin(w * x)
elif f == 'cos':
y = a * np.cos(w * x)
elif f == 'tan':
y = a * np.tan(w * x)
else: # x^2
y = a * x**2 / 10 # 缩放以适应图表
source.data = dict(x=x, y=y)
# 添加控件回调
function_select.on_change('value', update_data)
amplitude_slider.on_change('value', update_data)
frequency_slider.on_change('value', update_data)
# 创建布局
layout = column(
p,
row(function_select),
row(amplitude_slider, frequency_slider)
)
# 添加到文档
curdoc().add_root(layout)
curdoc().title = "Bokeh 服务器示例"
运行服务器:
bokeh serve --show bokeh_server_app.py
在 Bokeh 服务器中添加更复杂的回调:
# 保存为 streaming_data_app.py
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, Button, Div
from bokeh.layouts import column, row
from bokeh.io import curdoc
import numpy as np
from functools import partial
import time
# 初始化数据源
source = ColumnDataSource(data=dict(
x=[],
y=[]
))
# 创建图表
p = figure(title="实时数据流", width=800, height=400)
p.line('x', 'y', source=source, line_width=2, color="firebrick")
p.circle('x', 'y', source=source, size=6, color="navy", alpha=0.5)
# 创建状态显示
status_div = Div(text="状态: 就绪", width=200, height=30)
count_div = Div(text="数据点: 0", width=200, height=30)
# 创建控制按钮
start_button = Button(label="开始", button_type="success", width=100)
stop_button = Button(label="停止", button_type="danger", width=100)
clear_button = Button(label="清除", button_type="default", width=100)
# 设置初始状态
start_button.disabled = False
stop_button.disabled = True
data_streaming = False
start_time = time.time()
callback_id = None
# 添加新数据点
def update():
if not data_streaming:
return
# 获取当前数据
x = list(source.data['x'])
y = list(source.data['y'])
# 添加新数据点
current_time = time.time() - start_time
x.append(current_time)
y.append(np.sin(current_time) * np.random.normal(1, 0.1) + np.random.normal(0, 0.1))
# 保持最多100个点
if len(x) > 100:
x = x[-100:]
y = y[-100:]
# 更新数据源
source.data = dict(x=x, y=y)
# 更新计数
count_div.text = f"数据点: {len(x)}"
# 自动滚动x轴
if len(x) > 0:
p.x_range.start = max(0, x[-1] - 20)
p.x_range.end = x[-1] + 2
# 按钮回调函数
def start_streaming():
global data_streaming, start_time, callback_id
data_streaming = True
start_time = time.time() - (source.data['x'][-1] if len(source.data['x']) > 0 else 0)
start_button.disabled = True
stop_button.disabled = False
status_div.text = "状态: 数据流动中..."
callback_id = curdoc().add_periodic_callback(update, 100) # 100ms 更新一次
def stop_streaming():
global data_streaming, callback_id
data_streaming = False
start_button.disabled = False
stop_button.disabled = True
status_div.text = "状态: 已暂停"
if callback_id:
curdoc().remove_periodic_callback(callback_id)
def clear_data():
source.data = dict(x=[], y=[])
status_div.text = "状态: 已清除"
count_div.text = "数据点: 0"
p.x_range.start = 0
p.x_range.end = 10
# 绑定按钮回调
start_button.on_click(start_streaming)
stop_button.on_click(stop_streaming)
clear_button.on_click(clear_data)
# 创建布局
control_row = row(start_button, stop_button, clear_button)
info_row = row(status_div, count_div)
layout = column(info_row, p, control_row)
# 添加到文档
curdoc().add_root(layout)
curdoc().title = "数据流示例"
创建多页面 Bokeh 应用:
myapp/
|-- main.py # 主入口文件
|-- scatter.py # 散点图页面
|-- timeseries.py # 时间序列页面
|-- templates/ # 自定义模板文件夹
|-- index.html
main.py:
# main.py - 主入口文件
from bokeh.plotting import figure
from bokeh.models import Div, Tabs, Panel
from bokeh.layouts import column
from bokeh.io import curdoc
import scatter
import timeseries
# 创建欢迎页面
welcome_div = Div(
text="""
Bokeh 多页面仪表板
这是一个展示 Bokeh 多页面应用的示例。使用上方的标签页切换不同的可视化。
""",
width=800
)
# 创建标签页
panel1 = Panel(child=column(welcome_div), title="欢迎")
panel2 = Panel(child=scatter.layout, title="散点图")
panel3 = Panel(child=timeseries.layout, title="时间序列")
# 组合标签页
tabs = Tabs(tabs=[panel1, panel2, panel3])
# 添加到文档
curdoc().add_root(tabs)
curdoc().title = "Bokeh 多页面应用"
scatter.py:
# scatter.py - 散点图页面
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, Select, RangeSlider, ColorBar
from bokeh.layouts import column, row
from bokeh.transform import linear_cmap
import numpy as np
# 准备数据
N = 1000
x = np.random.normal(0, 1, N)
y = np.random.normal(0, 1, N)
color_values = np.random.uniform(-2, 2, N)
source = ColumnDataSource(data=dict(
x=x,
y=y,
radius=np.abs(np.random.normal(0, 1, N)) * 0.1,
colors=color_values
))
# 创建色彩映射
mapper = linear_cmap(field_name='colors', palette='Viridis256', low=-2, high=2)
# 创建图表
p = figure(title="交互式散点图", width=800, height=500)
scatter_plot = p.circle(
'x', 'y', size='radius', source=source,
fill_color=mapper, line_color="white", alpha=0.6,
hover_color="red", hover_alpha=0.8
)
# 添加颜色条
color_bar = ColorBar(color_mapper=mapper['transform'], width=8, location=(0,0))
p.add_layout(color_bar, 'right')
# 添加控件
point_size = RangeSlider(
title="点大小缩放", start=0.1, end=3.0, value=(1, 1), step=0.1
)
distribution_select = Select(
title="分布类型", value="正态分布",
options=["正态分布", "均匀分布", "指数分布"]
)
# 更新函数
def update_data(attr, old, new):
# 获取点大小范围
min_size, max_size = point_size.value
# 生成新数据
N = 1000
distribution = distribution_select.value
if distribution == "正态分布":
x = np.random.normal(0, 1, N)
y = np.random.normal(0, 1, N)
radius = np.abs(np.random.normal(0, 1, N)) * 0.1 * max_size
elif distribution == "均匀分布":
x = np.random.uniform(-2, 2, N)
y = np.random.uniform(-2, 2, N)
radius = np.random.uniform(0.01, 0.1, N) * max_size
else: # 指数分布
x = np.random.exponential(1, N) - 1
y = np.random.exponential(1, N) - 1
radius = np.random.exponential(0.1, N) * max_size
# 设置半径范围
radius = np.clip(radius, 0.01 * min_size, 0.5 * max_size)
# 更新数据
color_values = x * y # 基于x和y计算颜色值
source.data = dict(
x=x,
y=y,
radius=radius,
colors=color_values
)
# 绑定控件
distribution_select.on_change('value', update_data)
point_size.on_change('value', update_data)
# 创建布局
controls = column(distribution_select, point_size)
layout = row(controls, p)
timeseries.py:
# timeseries.py - 时间序列页面
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, DatetimeTickFormatter, Select, CheckboxGroup
from bokeh.layouts import column, row
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
# 生成时间序列数据
def generate_data(days=100, pattern="趋势"):
end_date = datetime.now()
start_date = end_date - timedelta(days=days)
dates = pd.date_range(start=start_date, end=end_date, freq='D')
n = len(dates)
trend = np.linspace(0, 5, n)
noise = np.random.normal(0, 0.5, n)
seasonal = 2 * np.sin(np.linspace(0, 4*np.pi, n))
if pattern == "趋势":
values = trend + noise
elif pattern == "季节性":
values = seasonal + noise
elif pattern == "趋势+季节性":
values = trend + seasonal + noise
else: # 随机
values = noise
return dates, values
# 初始数据
dates, values = generate_data()
source = ColumnDataSource(data=dict(
dates=dates,
values=values
))
# 创建图表
p = figure(title="时间序列分析", width=800, height=400, x_axis_type="datetime")
line = p.line('dates', 'values', source=source, line_width=2, line_color="navy")
circle = p.circle('dates', 'values', source=source, size=6, fill_color="white",
line_color="navy", alpha=0)
# 格式化时间轴
p.xaxis.formatter = DatetimeTickFormatter(
hours=["%H:%M"],
days=["%m-%d"],
months=["%Y-%m"],
years=["%Y"]
)
p.xaxis.major_label_orientation = np.pi/4
# 添加控件
pattern_select = Select(
title="数据模式", value="趋势",
options=["趋势", "季节性", "趋势+季节性", "随机"]
)
days_select = Select(
title="时间范围", value="100",
options=["30", "60", "100", "365"]
)
elements_checkbox = CheckboxGroup(
labels=["显示数据点", "显示移动平均线"], active=[]
)
# 更新函数
def update_data(attr, old, new):
# 生成新数据
days = int(days_select.value)
pattern = pattern_select.value
dates, values = generate_data(days, pattern)
# 计算移动平均线
window = max(3, int(len(values) * 0.05)) # 5%的窗口大小
moving_avg = np.convolve(values, np.ones(window)/window, mode='valid')
ma_dates = dates[window-1:]
# 更新数据源
source.data = dict(
dates=dates,
values=values,
ma_dates=ma_dates,
ma_values=moving_avg
)
# 更新可视元素
def update_elements(attr, old, new):
active = elements_checkbox.active
# 显示/隐藏数据点
circle.visible = 0 in active
# 添加/移除移动平均线
if 1 in active and 'ma_dates' in source.data:
if not hasattr(p, 'ma_line'):
p.ma_line = p.line('ma_dates', 'ma_values', source=source,
line_width=2, line_color="red",
line_dash="dashed", legend_label="移动平均线")
else:
p.ma_line.visible = True
elif hasattr(p, 'ma_line'):
p.ma_line.visible = False
# 绑定控件
pattern_select.on_change('value', update_data)
days_select.on_change('value', update_data)
elements_checkbox.on_change('active', update_elements)
# 创建布局
controls = column(pattern_select, days_select, elements_checkbox)
layout = row(controls, p)
# 初始更新
update_data(None, None, None)
运行多页面应用:
bokeh serve --show myapp/
Bokeh 允许使用 JavaScript 创建复杂的客户端交互:
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.models import CustomJS, ColumnDataSource, Button, TextInput
from bokeh.layouts import column, row
# 创建数据源
source = ColumnDataSource(data=dict(
x=[1, 2, 3, 4, 5],
y=[2, 5, 8, 2, 7]
))
# 创建图表
p = figure(title="自定义 JavaScript 回调示例", width=600, height=300)
p.circle('x', 'y', source=source, size=10, color="navy")
# 添加输入控件
x_input = TextInput(title="X坐标:", value="6")
y_input = TextInput(title="Y坐标:", value="4")
add_button = Button(label="添加点", button_type="success")
# 创建JavaScript回调
callback = CustomJS(args=dict(source=source, x_input=x_input, y_input=y_input), code="""
// 获取当前数据
const data = source.data;
const x_values = data['x'];
const y_values = data['y'];
// 获取输入值
const new_x = parseFloat(x_input.value);
const new_y = parseFloat(y_input.value);
// 验证输入
if (!isNaN(new_x) && !isNaN(new_y)) {
// 添加新点
x_values.push(new_x);
y_values.push(new_y);
// 提示用户
alert(`已添加新点(${new_x}, ${new_y})`);
// 通知数据源更新
source.change.emit();
} else {
alert("请输入有效的数值!");
}
""")
# 将回调绑定到按钮
add_button.js_on_click(callback)
# 创建布局
layout = column(
p,
row(x_input, y_input),
add_button
)
# 输出为HTML文件并显示
output_file("custom_js_callback.html")
show(layout)
# app.py
from flask import Flask, render_template
from bokeh.plotting import figure
from bokeh.embed import components
from bokeh.resources import INLINE
import numpy as np
app = Flask(__name__)
@app.route('/')
def home():
# 创建Bokeh图表
x = np.arange(0, 10, 0.1)
y = np.sin(x)
p = figure(title="Bokeh 与 Flask 集成", width=600, height=300)
p.line(x, y, line_width=2, color="navy")
# 生成组件
script, div = components(p)
# 获取JS和CSS资源
js_resources = INLINE.render_js()
css_resources = INLINE.render_css()
# 渲染模板
return render_template(
'index.html',
plot_script=script,
plot_div=div,
js_resources=js_resources,
css_resources=css_resources,
)
if __name__ == '__main__':
app.run(debug=True)
HTML模板 (templates/index.html):
DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bokeh 与 Flask 集成title>
{{ css_resources|safe }}
{{ js_resources|safe }}
{{ plot_script|safe }}
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.content {
padding: 20px;
background-color: white;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
}
style>
head>
<body>
<div class="content">
<h1>Bokeh 与 Flask 集成示例h1>
<p>这是一个展示如何将 Bokeh 可视化集成到 Flask 应用的示例。p>
<div>
{{ plot_div|safe }}
div>
div>
body>
html>
# views.py
from django.shortcuts import render
from bokeh.plotting import figure
from bokeh.embed import components
from bokeh.resources import CDN
import numpy as np
def bokeh_example(request):
# 创建Bokeh图表
x = np.arange(0, 10, 0.1)
y = np.cos(x)
p = figure(title="Bokeh 与 Django 集成", width=600, height=300)
p.line(x, y, line_width=2, color="firebrick")
# 生成组件
script, div = components(p)
# 传递组件到模板
context = {
'plot_script': script,
'plot_div': div,
'cdn_js': CDN.js_files[0],
'cdn_css': CDN.css_files[0],
}
return render(request, 'bokeh_example.html', context)
HTML模板 (templates/bokeh_example.html):
{% load static %}
DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bokeh 与 Django 集成title>
<link rel="stylesheet" href="{{ cdn_css }}">
<script src="{{ cdn_js }}">script>
{{ plot_script|safe }}
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.content {
padding: 20px;
background-color: white;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
}
style>
head>
<body>
<div class="content">
<h1>Bokeh 与 Django 集成示例h1>
<p>这是一个展示如何将 Bokeh 可视化集成到 Django 应用的示例。p>
<div>
{{ plot_div|safe }}
div>
div>
body>
html>
处理大型数据集时的优化策略:
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.models import ColumnDataSource, HoverTool, RangeSlider
from bokeh.layouts import column
import numpy as np
import pandas as pd
import time
# 创建大型数据集
def create_large_dataset(n=100000):
print(f"生成 {n} 个数据点...")
df = pd.DataFrame({
'x': np.random.normal(0, 1, n),
'y': np.random.normal(0, 1, n),
'size': np.abs(np.random.normal(0, 1, n)) * 5,
'color': np.random.normal(0, 1, n)
})
return df
# 1. 数据下采样策略
def downsample_data(df, sample_size=10000):
if len(df) > sample_size:
return df.sample(sample_size)
return df
# 2. 数据分块策略
def create_progressive_views(df, n_views=5):
total_points = len(df)
views = []
# 创建逐渐增加数据点的视图
for i in range(n_views):
sample_size = int(total_points * (i + 1) / n_views)
view = df.iloc[:sample_size]
views.append(view)
return views
# 3. WebGL渲染
# 主要图表
df = create_large_dataset(200000)
df_downsampled = downsample_data(df)
source = ColumnDataSource(df_downsampled)
p = figure(title="大数据集渲染优化",
width=800, height=600,
output_backend="webgl") # 使用WebGL后端渲染
# 添加散点图
scatter = p.circle(
'x', 'y',
source=source,
size='size',
color={'field': 'color', 'transform': 'linear_cmap(field_name="color", palette="Viridis256", low=-3, high=3)'},
alpha=0.5
)
# 添加悬停工具
hover = HoverTool(renderers=[scatter], tooltips=[
("索引", "$index"),
("(x,y)", "($x, $y)"),
("大小", "@size")
])
p.add_tools(hover)
# 添加数据量滑块
def update_points(attr, old, new):
# 获取滑块值
data_percentage = slider.value / 100
# 计算数据点数量并更新
n_points = int(len(df) * data_percentage)
new_data = df.iloc[:n_points]
source.data = ColumnDataSource.from_df(new_data)
# 更新标题
p.title.text = f"大数据集渲染优化 (显示 {n_points:,}/{len(df):,} 个数据点)"
slider = RangeSlider(
title="数据显示百分比",
start=1,
end=100,
value=(1, 5), # 默认显示5%的数据
step=1
)
slider.on_change('value', update_points)
# 创建布局
layout = column(p, slider)
# 输出为HTML文件并显示
output_file("large_dataset_optimization.html")
show(layout)
使用 Bokeh 创建股票价格可视化:
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, NumeralTickFormatter, DatetimeTickFormatter, Button, CustomJS
from bokeh.layouts import column, row
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# 生成模拟股票数据
def generate_stock_data():
end_date = datetime.now()
start_date = end_date - timedelta(days=365)
dates = pd.date_range(start=start_date, end=end_date, freq='B') # 工作日
# 初始价格
price = 100
prices = [price]
# 生成随机价格序列
np.random.seed(42)
for i in range(1, len(dates)):
change = np.random.normal(0, 1) * 2 + 0.1 # 小偏度使价格有上升趋势
price *= (1 + change / 100)
prices.append(price)
# 计算成交量
volume = np.random.normal(1000000, 200000, len(dates))
volume = np.abs(volume)
# 计算移动平均线
ma20 = pd.Series(prices).rolling(window=20).mean().tolist()
ma50 = pd.Series(prices).rolling(window=50).mean().tolist()
ma200 = pd.Series(prices).rolling(window=200).mean().tolist()
# 创建DataFrame
df = pd.DataFrame({
'date': dates,
'price': prices,
'volume': volume,
'ma20': ma20,
'ma50': ma50,
'ma200': ma200
})
# 添加涨跌信息
df['change'] = df['price'].pct_change() * 100
df['up'] = df['change'] >= 0
return df
# 生成数据
stock_data = generate_stock_data()
source = ColumnDataSource(stock_data)
# 创建K线图
kfig = figure(
title='股票价格走势图',
x_axis_type='datetime',
width=1000,
height=400,
tools='pan,wheel_zoom,box_zoom,reset,save',
toolbar_location='above'
)
# 添加K线
kfig.line('date', 'price', source=source, line_width=2, color='black', alpha=0.5, legend_label="收盘价")
# 添加移动平均线
kfig.line('date', 'ma20', source=source, color='blue', line_width=1.5, legend_label="20日均线")
kfig.line('date', 'ma50', source=source, color='green', line_width=1.5, legend_label="50日均线")
kfig.line('date', 'ma200', source=source, color='red', line_width=1.5, legend_label="200日均线")
# 设置图例
kfig.legend.location = "top_left"
kfig.legend.click_policy = "hide"
# 添加坐标轴格式
kfig.xaxis.formatter = DatetimeTickFormatter(
days=["%Y-%m-%d"],
months=["%Y-%m"],
years=["%Y"]
)
kfig.yaxis.formatter = NumeralTickFormatter(format="$0,0.00")
# 添加十字线工具
crosshair = CrosshairTool(
dimensions="both",
line_color='gray',
line_alpha=0.5,
line_width=1
)
kfig.add_tools(crosshair)
# 添加悬停信息
hover = HoverTool(
tooltips=[
('日期', '@date{%F}'),
('价格', '@price{$0,0.00}'),
('涨跌', '@change{+0.00}%'),
('成交量', '@volume{0.00a}'),
],
formatters={
'@date': 'datetime',
},
mode='vline'
)
kfig.add_tools(hover)
# 创建成交量图表
vfig = figure(
x_axis_type='datetime',
width=1000,
height=200,
tools='',
toolbar_location=None,
x_range=kfig.x_range
)
# 添加成交量柱状图,根据涨跌显示不同颜色
vfig.vbar(
x='date', top='volume',
width=timedelta(days=0.7),
color='green', source=source,
view=ColumnDataSource.view(source, stock_data.index[stock_data['up']])
)
vfig.vbar(
x='date', top='volume',
width=timedelta(days=0.7),
color='red', source=source,
view=ColumnDataSource.view(source, stock_data.index[~stock_data['up']])
)
# 设置坐标轴格式
vfig.yaxis.formatter = NumeralTickFormatter(format="0.00a")
vfig.yaxis.axis_label = "成交量"
# 添加时间范围选择按钮
one_month = Button(label="1月", width=60)
three_months = Button(label="3月", width=60)
six_months = Button(label="6月", width=60)
one_year = Button(label="1年", width=60)
all_data = Button(label="全部", width=60)
range_callback = CustomJS(args=dict(
kfig=kfig,
data=source,
one_month=30,
three_months=90,
six_months=180,
one_year=365), code="""
const end_date = new Date(Math.max(...data.data.date));
let start_date;
// 获取触发按钮
const button_label = cb_obj.label;
if (button_label === "1月") {
start_date = new Date(end_date.getTime() - one_month * 24 * 60 * 60 * 1000);
} else if (button_label === "3月") {
start_date = new Date(end_date.getTime() - three_months * 24 * 60 * 60 * 1000);
} else if (button_label === "6月") {
start_date = new Date(end_date.getTime() - six_months * 24 * 60 * 60 * 1000);
} else if (button_label === "1年") {
start_date = new Date(end_date.getTime() - one_year * 24 * 60 * 60 * 1000);
} else {
// 全部数据
start_date = new Date(Math.min(...data.data.date));
}
kfig.x_range.start = start_date;
kfig.x_range.end = end_date;
""")
one_month.js_on_click(range_callback)
three_months.js_on_click(range_callback)
six_months.js_on_click(range_callback)
one_year.js_on_click(range_callback)
all_data.js_on_click(range_callback)
# 整合图表
layout = column(
row(one_month, three_months, six_months, one_year, all_data),
kfig,
vfig
)
# 显示图表
output_file("stock_visualization.html")
show(layout)
创建交互式地图可视化:
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.models import GeoJSONDataSource, LinearColorMapper, ColorBar, HoverTool
from bokeh.palettes import Viridis256
import json
import pandas as pd
import requests
from io import StringIO
# 下载中国省份GeoJSON数据 (示例使用了在线数据源)
geojson_url = "https://raw.githubusercontent.com/apache/echarts/master/map/json/china.json"
response = requests.get(geojson_url)
china_geojson = response.json()
# 准备省份数据(示例数据)
provinces_data = pd.DataFrame({
'name': ['北京', '上海', '广东', '江苏', '浙江', '四川', '湖北', '河南', '辽宁', '山东'],
'value': [178, 195, 220, 183, 176, 160, 155, 170, 145, 178]
})
# 将数据与GeoJSON整合
for feature in china_geojson['features']:
province_name = feature['properties']['name']
province_value = provinces_data[provinces_data['name'] == province_name]['value'].values
if len(province_value) > 0:
feature['properties']['value'] = float(province_value[0])
else:
feature['properties']['value'] = 0
# 创建GeoJSON数据源
geosource = GeoJSONDataSource(geojson=json.dumps(china_geojson, ensure_ascii=False))
# 创建颜色映射
color_mapper = LinearColorMapper(palette=Viridis256, low=provinces_data['value'].min(), high=provinces_data['value'].max())
# 创建图表
p = figure(
title='中国省份数据地图',
width=800,
height=600,
toolbar_location='right',
tools='pan,wheel_zoom,box_zoom,reset,save'
)
# 绘制地图
provinces = p.patches(
xs='xs',
ys='ys',
source=geosource,
fill_color={'field': 'value', 'transform': color_mapper},
line_color='black',
line_width=0.5,
fill_alpha=0.7
)
# 添加颜色条
color_bar = ColorBar(
color_mapper=color_mapper,
label_standoff=12,
border_line_color=None,
location=(0, 0)
)
p.add_layout(color_bar, 'right')
# 添加悬停工具
hover = HoverTool(renderers=[provinces], tooltips=[
('省份', '@name'),
('值', '@value{0.0}')
])
p.add_tools(hover)
# 移除坐标轴
p.axis.visible = False
p.grid.visible = False
# 显示地图
output_file("china_map.html")
show(p)
创建科学数据可视化:
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.layouts import gridplot
from bokeh.models import ColumnDataSource, ColorBar, LinearColorMapper, HoverTool
from bokeh.transform import linear_cmap
from bokeh.palettes import Viridis256
import numpy as np
from scipy import stats
# 生成科学数据
np.random.seed(42)
# 1. 2D高斯分布
n = 1000
X = np.random.multivariate_normal([0, 0], [[1, 0.5], [0.5, 1]], n)
# 2. 用于等高线的数据
x = np.linspace(-3, 3, 100)
y = np.linspace(-3, 3, 100)
X_grid, Y_grid = np.meshgrid(x, y)
pos = np.dstack((X_grid, Y_grid))
rv = stats.multivariate_normal([0, 0], [[1, 0.5], [0.5, 1]])
Z = rv.pdf(pos)
# 3. 用于3D曲面的数据
theta = np.linspace(0, 4*np.pi, 100)
r = np.linspace(0, 2, 100)
THETA, R = np.meshgrid(theta, r)
X_3d = R * np.cos(THETA)
Y_3d = R * np.sin(THETA)
Z_3d = np.sin(R) * np.cos(THETA)
# 创建散点图
scatter_source = ColumnDataSource(data=dict(
x=X[:, 0],
y=X[:, 1],
dist=np.sqrt(X[:, 0]**2 + X[:, 1]**2)
))
scatter_mapper = linear_cmap(field_name='dist', palette=Viridis256, low=0, high=3)
p1 = figure(title="2D高斯分布散点图", width=400, height=400)
scatter = p1.circle(
'x', 'y', source=scatter_source,
size=8, color=scatter_mapper, alpha=0.6
)
hover = HoverTool(renderers=[scatter], tooltips=[
("x", "@x{0.00}"),
("y", "@y{0.00}"),
("距离", "@dist{0.00}")
])
p1.add_tools(hover)
color_bar = ColorBar(
color_mapper=scatter_mapper['transform'],
width=8,
location=(0, 0)
)
p1.add_layout(color_bar, 'right')
# 创建等高线图
contour_source = ColumnDataSource(data=dict(
x=X_grid.flatten(),
y=Y_grid.flatten(),
z=Z.flatten()
))
contour_mapper = LinearColorMapper(palette=Viridis256, low=np.min(Z), high=np.max(Z))
p2 = figure(title="高斯分布等高线图", width=400, height=400)
contour = p2.image(
image=[Z],
x=-3, y=-3, dw=6, dh=6,
palette=Viridis256
)
contour_color_bar = ColorBar(
color_mapper=contour_mapper,
width=8,
location=(0, 0)
)
p2.add_layout(contour_color_bar, 'right')
# 创建3D曲面可视化(使用图像表示)
surface_mapper = LinearColorMapper(palette=Viridis256, low=np.min(Z_3d), high=np.max(Z_3d))
p3 = figure(title="3D曲面可视化", width=400, height=400)
surface = p3.image(
image=[Z_3d],
x=np.min(X_3d), y=np.min(Y_3d),
dw=np.max(X_3d)-np.min(X_3d), dh=np.max(Y_3d)-np.min(Y_3d),
palette=Viridis256
)
surface_color_bar = ColorBar(
color_mapper=surface_mapper,
width=8,
location=(0, 0)
)
p3.add_layout(surface_color_bar, 'right')
# 创建统计直方图
hist, edges = np.histogram(np.sqrt(X[:, 0]**2 + X[:, 1]**2), bins=50)
hist_source = ColumnDataSource(data=dict(
top=hist,
left=edges[:-1],
right=edges[1:]
))
p4 = figure(title="径向距离分布", width=400, height=400)
p4.quad(
top='top', bottom=0,
left='left', right='right',
source=hist_source,
fill_color="navy", line_color="white", alpha=0.7
)
# 设置坐标轴标签
p1.xaxis.axis_label = "X轴"
p1.yaxis.axis_label = "Y轴"
p2.xaxis.axis_label = "X轴"
p2.yaxis.axis_label = "Y轴"
p3.xaxis.axis_label = "X轴"
p3.yaxis.axis_label = "Y轴"
p4.xaxis.axis_label = "径向距离"
p4.yaxis.axis_label = "频数"
# 组合图表
grid = gridplot([
[p1, p2],
[p3, p4]
], width=400, height=400)
# 显示图表
output_file("scientific_visualization.html")
show(grid)
# 推荐做法
from bokeh.models import ColumnDataSource
source = ColumnDataSource(data=dict(
x=[1, 2, 3, 4, 5],
y=[2, 5, 8, 2, 7]
))
p.circle('x', 'y', source=source, ...)
# 而不是直接传递列表
# p.circle([1, 2, 3, 4, 5], [2, 5, 8, 2, 7], ...)
def create_figure(width=600, height=400, title="图表标题"):
"""创建基本图表对象"""
p = figure(width=width, height=height, title=title)
# 添加公共设置
p.xaxis.axis_label = "X轴"
p.yaxis.axis_label = "Y轴"
return p
def add_scatter_layer(fig, x, y, color="navy", size=8):
"""添加散点图层"""
return fig.circle(x, y, color=color, size=size, alpha=0.6)
def add_line_layer(fig, x, y, color="firebrick", width=2):
"""添加线图层"""
return fig.line(x, y, line_color=color, line_width=width)
# 使用时
p = create_figure(title="多层图表")
add_scatter_layer(p, [1, 2, 3, 4, 5], [2, 5, 8, 2, 7])
add_line_layer(p, [1, 2, 3, 4, 5], [2, 5, 8, 2, 7])
from bokeh.io import curdoc
from bokeh.themes import Theme
# 创建自定义主题
theme_json = {
'attrs': {
'Figure': {
'background_fill_color': '#f5f5f5',
'border_fill_color': 'white',
'outline_line_color': '#444444',
},
'Axis': {
'axis_line_color': '#444444',
'major_tick_line_color': '#444444',
'minor_tick_line_color': '#444444',
},
'Grid': {
'grid_line_color': '#dddddd',
},
'Title': {
'text_font_size': '12pt',
'text_font_style': 'bold',
}
}
}
# 应用主题
curdoc().theme = Theme(json=theme_json)
from bokeh.models import LayoutDOM
from bokeh.layouts import row, column
# 确保布局响应式
def create_responsive_layout(main_content, sidebar):
return row(
sidebar,
main_content,
sizing_mode="stretch_both" # 填充可用空间
)
# 单个图表使用固定尺寸与比例
p = figure(
width=600,
height=400,
sizing_mode="scale_width" # 保持宽高比,但根据宽度缩放
)
# 在服务器应用中使用节流限制更新频率
from functools import lru_cache
import time
# 缓存数据加载函数
@lru_cache(maxsize=32)
def load_data(source_name):
# 模拟耗时操作
print(f"加载数据: {source_name}")
time.sleep(2)
return np.random.randn(1000)
# 为重复计算添加缓存装饰器
def throttle(delay):
"""创建限制函数调用频率的装饰器"""
def decorator(func):
last_called = 0
result = None
def wrapper(*args, **kwargs):
nonlocal last_called, result
current_time = time.time()
if current_time - last_called > delay:
result = func(*args, **kwargs)
last_called = current_time
return result
return wrapper
return decorator
# 使用节流装饰器限制回调频率
@throttle(delay=0.5) # 至少间隔0.5秒
def update_plot(attr, old, new):
# 更新图表逻辑...
pass
# 排查:确保调用了show()函数
from bokeh.plotting import show
show(p)
# 排查:检查数据是否为空
print("数据长度:", len(source.data['x']))
# 排查:检查坐标轴范围是否合适
print("X轴范围:", p.x_range.start, p.x_range.end)
print("Y轴范围:", p.y_range.start, p.y_range.end)
# 解决:明确设置坐标轴范围
p.x_range.start = 0
p.x_range.end = 10
p.y_range.start = 0
p.y_range.end = 10
# 排查:检查触发器是否连接正确
print("已连接回调:", button.js_on_click.callbacks)
# 排查:检查输入输出是否匹配
print("组件的属性:", dir(dropdown))
# 解决:调试JavaScript回调
callback = CustomJS(args=dict(source=source), code="""
console.log("回调触发");
console.log("数据源:", source.data);
// 更多调试代码...
source.change.emit(); // 确保触发变更事件
""")
# 启用详细日志
bokeh serve --show myapp/ --log-level debug
# 在应用中添加错误处理
try:
# 可能出错的代码
result = process_data(input_data)
except Exception as e:
# 记录错误
import logging
logging.error(f"处理数据时出错: {e}")
# 提供用户友好的错误消息
error_div.text = f"""
处理数据时发生错误,请检查输入数据。
错误详情: {str(e)}
"""
# 返回默认值
result = default_data
# 使用WebGL渲染提高性能
p = figure(output_backend="webgl")
# 减少数据点
if len(data) > 10000:
data = data.sample(10000)
# 禁用不必要的工具
p = figure(tools="pan,box_zoom,reset") # 仅保留必要的工具
# 优化悬停性能
hover = HoverTool(
tooltips=[("x", "@x"), ("y", "@y")],
mode='mouse', # 使用鼠标模式而非vline
point_policy='snap_to_data', # 捕捉到最近的数据点
line_policy='nearest' # 仅显示最近的线
)
Bokeh 作为 Python 生态系统中的交互式可视化库,具有以下优势:
Bokeh 特别适合以下场景:
特性 | Bokeh | Matplotlib | Plotly | HoloViews | Panel |
---|---|---|---|---|---|
交互性 | 丰富 | 有限 | 丰富 | 丰富 | 丰富 |
学习曲线 | 中等 | 低 | 中等 | 中等 | 中等 |
适合数据集大小 | 大型 | 中小型 | 中大型 | 大型 | 大型 |
网页兼容性 | 原生 | 需转换 | 原生 | 原生 | 原生 |
与 Pandas 集成 | 良好 | 极佳 | 极佳 | 良好 | 良好 |
服务器能力 | 内置 | 无 | 有(Dash) | 通过Bokeh | 内置 |
代码简洁性 | 中等 | 冗长 | 中等 | 高 | 高 |
自定义能力 | 高 | 极高 | 高 | 中等 | 中等 |
文档质量 | 良好 | 极佳 | 极佳 | 中等 | 良好 |
选择合适的可视化库的建议:
Bokeh 在不断发展,未来方向包括:
Bokeh 提供了一套强大的工具,让 Python 开发者能够创建专业级的交互式数据可视化。无论是用于数据分析、科学研究还是构建完整的仪表板应用,Bokeh 都提供了灵活而强大的解决方案。通过本文的介绍,您应该已经掌握了 Bokeh 的基础知识和高级技术,能够开始构建自己的交互式可视化项目。
随着数据可视化需求的增长,Bokeh 的重要性也将不断提升。通过继续学习和实践,您可以充分发挥 Bokeh 的潜力,创建令人印象深刻的交互式数据可视化作品。