【交互式数据可视化神器】Bokeh完全指南:10种高级图表+实战案例 | Python开发必备技能

Bokeh:Python交互式可视化开发

一、Bokeh 简介

Bokeh 是一个针对现代 Web 浏览器的交互式可视化库,专注于为大型数据集提供优雅、简洁的呈现。与 Matplotlib 和 Seaborn 等传统静态可视化库不同,Bokeh 生成的是在浏览器中渲染的交互式图表,具有缩放、平移、悬停等丰富的互动功能。

Bokeh 的核心理念是创建面向 Web 的可视化工具,它使用现代 Web 技术(如 HTML5 Canvas 和 WebGL)实现高性能图形渲染,无需依赖 JavaScript 编程即可在 Python 中构建复杂的可视化项目。

1.1 Bokeh 的主要特点

  • 原生交互性:无需额外代码即可支持缩放、平移、选择、悬停等交互操作
  • 高性能渲染:基于 HTML5 Canvas/WebGL 渲染,支持大数据集可视化
  • 服务器组件:支持 Bokeh 服务器实现更复杂的交互和数据流
  • 灵活的输出:支持输出为独立 HTML 文件、Jupyter Notebook 或嵌入到 Django/Flask 应用中
  • 无 JavaScript 依赖:纯 Python 接口,无需编写 JavaScript 代码
  • 链式 API:支持链式调用的 API 设计,代码简洁易读
  • 可扩展性:从基本图表到复杂仪表板都能支持

1.2 与其他可视化库的比较

特性 Bokeh Matplotlib Plotly D3.js
交互性 原生支持 有限 原生支持 完全支持
开发语言 Python Python Python/R/JS JavaScript
输出格式 HTML/服务器 图片/PDF HTML/服务器 HTML
大数据支持 良好 有限 良好 有限
学习曲线 中等 平缓 中等 陡峭
编程模型 声明式/命令式 命令式 声明式/命令式 声明式
独立性 可独立使用 可独立使用 依赖云服务(免费版) 需要网页环境

1.3 Bokeh 的应用场景

Bokeh 特别适合以下应用场景:

  1. 数据探索与分析:交互式探索大型数据集
  2. 科学计算可视化:复杂数据关系的动态展示
  3. Web 应用集成:与 Django、Flask 等框架集成创建数据可视化应用
  4. 流数据可视化:结合 Bokeh Server 展示实时变化的数据
  5. 地理空间可视化:地图数据的交互式展示
  6. 金融数据分析:股票、交易数据的动态图表

二、安装与环境配置

2.1 安装 Bokeh

# 使用 pip 安装
pip install bokeh

# 或使用 conda 安装
conda install bokeh -c conda-forge

2.2 基础依赖

Bokeh 的核心依赖包括:

  • NumPy:用于数值计算
  • Pillow:用于图像处理
  • Jinja2:用于模板渲染
  • PyYAML:用于配置文件处理
  • Tornado:用于 Bokeh 服务器(可选,如果使用服务器功能)

2.3 验证安装

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)

2.4 开发环境设置

对于 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 基础绘图

3.1 绘图核心概念

Bokeh 的绘图架构包含以下核心概念:

  • Figure:绘图的容器,相当于画布
  • Glyphs:图形元素,如圆形、线条、矩形等
  • Tools:交互工具,如缩放、平移、选择等
  • Layouts:布局系统,如行、列、网格等
  • Models:低级组件,构成 Bokeh 可视化的基本元素

3.2 创建基本图表

散点图
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)

3.3 图形属性设置

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)

3.4 组合多种图形

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)

四、交互特性

4.1 基本交互工具

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)

4.2 自定义悬停工具提示

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)
@desc
"""
) p.add_tools(hover) # 输出为HTML文件并显示 output_file("custom_hover.html") show(p)

4.3 选择与链接

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)

4.4 滑块和按钮交互

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)

五、布局与结构

5.1 基本布局系统

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)

5.2 标签页与面板

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)

5.3 自定义模板与嵌入

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 服务器应用

6.1 服务器基础

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

6.2 添加回调和数据流

在 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 = "数据流示例"

6.3 多页面应用

创建多页面 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/

七、高级技术

7.1 自定义 JavaScript 回调

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)

7.2 与Web框架集成

与 Flask 集成
# 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>
与 Django 集成
# 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>

7.3 大数据集性能优化

处理大型数据集时的优化策略:

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)

八、实际应用案例

8.1 金融数据可视化

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

8.2 地理空间数据可视化

创建交互式地图可视化:

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)

8.3 科学数据可视化

创建科学数据可视化:

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)

九、最佳实践与问题排查

9.1 Bokeh 开发最佳实践

  1. 使用 ColumnDataSource:尽可能使用 ColumnDataSource 来管理数据,它提供了更好的互操作性。
# 推荐做法
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], ...)
  1. 模块化设计:将应用分解为独立的函数和组件。
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])
  1. 使用主题:保持可视化风格一致。
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)
  1. 控制图表大小:为不同设备设置合适的尺寸。
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"  # 保持宽高比,但根据宽度缩放
)
  1. 优化性能:避免过多的实时更新和大数据集。
# 在服务器应用中使用节流限制更新频率
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

9.2 常见问题排查

  1. 图表不显示
# 排查:确保调用了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
  1. 回调不工作
# 排查:检查触发器是否连接正确
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();  // 确保触发变更事件
""")
  1. 服务器应用异常
# 启用详细日志
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
  1. 性能问题
# 使用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'  # 仅显示最近的线
)

十、总结与比较

10.1 Bokeh 的优势与适用场景

Bokeh 作为 Python 生态系统中的交互式可视化库,具有以下优势:

  1. 原生交互性:无需额外代码即可获得缩放、平移等互动功能
  2. Web 友好:生成的可视化可直接在网页浏览器中查看
  3. 大数据支持:支持大规模数据集的高效渲染
  4. 服务器能力:通过 Bokeh Server 实现复杂的交互和实时更新
  5. 与 Web 框架集成:容易与 Flask、Django 等框架结合
  6. 纯 Python 接口:不需要 JavaScript 知识

Bokeh 特别适合以下场景:

  • 交互式仪表板:需要用户进行数据探索和交互
  • Web 应用集成:需要嵌入到 Web 应用的可视化
  • 大型数据集:需要高效渲染大量数据点
  • 实时数据监控:需要动态更新的数据流可视化
  • 地理空间可视化:地图和地理信息的展示

10.2 与其他可视化库的比较

特性 Bokeh Matplotlib Plotly HoloViews Panel
交互性 丰富 有限 丰富 丰富 丰富
学习曲线 中等 中等 中等 中等
适合数据集大小 大型 中小型 中大型 大型 大型
网页兼容性 原生 需转换 原生 原生 原生
与 Pandas 集成 良好 极佳 极佳 良好 良好
服务器能力 内置 有(Dash) 通过Bokeh 内置
代码简洁性 中等 冗长 中等
自定义能力 极高 中等 中等
文档质量 良好 极佳 极佳 中等 良好

选择合适的可视化库的建议:

  • 静态图表和科学出版:选择 Matplotlib
  • 快速数据探索和统计分析:选择 Seaborn
  • 交互式 Web 可视化:选择 Bokeh 或 Plotly
  • 数据密集型可视化:选择 HoloViews 或 Datashader
  • 完整的仪表板应用:选择 Dash 或 Panel

10.3 Bokeh 的未来发展

Bokeh 在不断发展,未来方向包括:

  1. 性能优化:持续改进大数据集的渲染性能
  2. AI 集成:提供与机器学习和人工智能模型的更好集成
  3. 3D 可视化:增强三维可视化能力
  4. 移动友好:更好地支持移动设备
  5. 实时协作:支持多用户协作可视化
  6. 扩展生态系统:与更多数据科学工具的整合

结语

Bokeh 提供了一套强大的工具,让 Python 开发者能够创建专业级的交互式数据可视化。无论是用于数据分析、科学研究还是构建完整的仪表板应用,Bokeh 都提供了灵活而强大的解决方案。通过本文的介绍,您应该已经掌握了 Bokeh 的基础知识和高级技术,能够开始构建自己的交互式可视化项目。

随着数据可视化需求的增长,Bokeh 的重要性也将不断提升。通过继续学习和实践,您可以充分发挥 Bokeh 的潜力,创建令人印象深刻的交互式数据可视化作品。

资源与参考

  • Bokeh 官方文档
  • Bokeh GitHub 仓库
  • Bokeh 示例库
  • Bokeh 用户指南
  • Bokeh 服务器参考

你可能感兴趣的:(技术技巧,#,可视化工具,python,开发语言)