第二章:核心概念
前置知识:建议你先完成第一章。
延伸阅读:
本章只回答四个问题:
- Bokeh 为什么强调
ColumnDataSource Figure、Glyph、Document分别是什么- Python 代码为什么能在浏览器里变成交互图表
- 选择、悬停、更新这些交互是怎么工作的
2.1 一张图是怎么组成的?
先看最常见的一段代码:
from bokeh.plotting import figure, show
p = figure(title="示例")
p.circle([1, 2, 3], [4, 5, 6], size=12)
show(p)
这段代码背后其实包含了几层对象:
Figure:整张图的容器Glyph:具体怎么画,比如圆、线、柱子DataSource:图表使用的数据Document:把整套对象组织起来,交给 Bokeh 输出或同步BokehJS:浏览器端负责渲染和交互的引擎
你可以先把它记成一句话:
Bokeh 会把 Python 中的图表对象组织成一个文档,再交给浏览器中的 BokehJS 渲染。
2.2 为什么推荐 ColumnDataSource
2.2.1 直接传数组可以画图,但不适合复杂交互
最简单的写法是直接传列表:
from bokeh.plotting import figure, show
p = figure()
p.circle([1, 2, 3], [10, 20, 30], size=10)
show(p)
这样写很适合快速试验,但一旦你想做下面这些事,就会开始不方便:
- 悬停时显示更多字段
- 用某一列控制颜色或大小
- 多个图共享同一份数据
- 在 Bokeh Server 中动态更新数据
- 做选择联动
2.2.2 ColumnDataSource 是 Bokeh 的标准数据入口
from bokeh.models import ColumnDataSource, HoverTool
from bokeh.plotting import figure, show
source = ColumnDataSource(data={
"product": ["A", "B", "C"],
"sales": [100, 200, 150],
"price": [10, 20, 15],
})
p = figure(title="销量与价格")
p.circle("sales", "price", source=source, size=14)
p.add_tools(HoverTool(tooltips=[
("产品", "@product"),
("销量", "@sales"),
("价格", "@price"),
]))
show(p)
这里的关键变化是:
- 图形不再写死列表,而是写列名:
"sales"、"price" HoverTool可以通过@product、@sales读取同一行数据- 同一个
source可以被多个图、多个工具、多个回调共享
2.2.3 把它理解成“按列存储的表”
ColumnDataSource 的核心结构就是一个字典:
source.data = {
"x": [1, 2, 3],
"y": [4, 5, 6],
"label": ["A", "B", "C"],
}
你可以把它当成一张表:
| x | y | label |
|---|---|---|
| 1 | 4 | A |
| 2 | 5 | B |
| 3 | 6 | C |
重要约束:每一列长度必须一致。
因为第 i 行的数据会被当成同一个数据点的完整属性。
2.3 ColumnDataSource 的常见操作
在继续往下看之前,先记住一个经验:
只要图表需要悬停、多图联动、筛选、回调或增量更新,就尽量尽早切到
ColumnDataSource。
因为后面第四章:数据转换与处理里的过滤器、映射和视图,和第六章:交互功能场景实战里的联动、回调、刷选,几乎都默认你已经把数据放进了 ColumnDataSource。
2.3.1 创建方式
from bokeh.models import ColumnDataSource
import pandas as pd
# 方式 1:直接传字典
source = ColumnDataSource(data={
"x": [1, 2, 3],
"y": [4, 5, 6],
})
# 方式 2:从 DataFrame 创建
df = pd.DataFrame({
"x": [1, 2, 3],
"y": [4, 5, 6],
})
source = ColumnDataSource(df)
2.3.2 读取和整体替换
print(source.data)
print(source.data["x"])
source.data = {
"x": [10, 20],
"y": [30, 40],
}
当你执行整体替换时,Bokeh 会把这视为一次完整的数据更新。
2.3.3 增量更新:stream() 和 patch()
# 追加新行
source.stream({
"x": [4],
"y": [7],
})
# 局部修改
source.patch({
"y": [(0, 100)],
})
适用场景:
stream():实时追加数据,例如传感器、日志、时间序列patch():只改局部值,减少更新量
2.3.4 选择状态
selected = source.selected.indices
print(selected)
source.selected.indices = [1, 3]
source.selected.indices = []
这套选择状态是 Bokeh 联动机制的重要基础。
多个图如果共享同一个 ColumnDataSource,选择状态也会共享。
2.3.5 ColumnDataSource 的常见坑
这是初学者最容易卡住的一组问题。很多“图不显示”“悬停取不到值”“回调没反应”的根源,其实都在这里。
坑 1:各列长度不一致
这是最常见的问题。
source = ColumnDataSource(data={
"x": [1, 2, 3],
"y": [4, 5], # 错误:长度不一致
})
为什么不行:
ColumnDataSource本质上是“按列存储的一张表”- 第
i行的数据会被当成一个完整数据点 - 所以每一列都必须能对齐到同样的行数
排查方式:
- 打印
source.data - 分别检查每一列长度
- 一旦你从 DataFrame、字典、回调中动态拼装数据,这一步尤其要做
坑 2:字段名写错,但代码表面上看不出来
source = ColumnDataSource(data={
"sales": [100, 200, 150],
"price": [10, 20, 15],
})
p.circle("sale", "price", source=source) # "sale" 拼错了
这里最难受的地方在于:
- Python 端代码可能看起来没什么问题
- 但浏览器里图形不会按预期显示
HoverTool、颜色映射、回调里也可能因为列名拼错而失效
建议:
- 先打印
source.column_names - 在 tooltip、glyph 字段、transform 字段、回调代码里统一复用同一套列名
- 字段一多时,优先把列名集中定义,减少手写字符串的次数
坑 3:在 DataFrame 和 ColumnDataSource 之间切换时忽略了索引
import pandas as pd
from bokeh.models import ColumnDataSource
df = pd.DataFrame({
"x": [1, 2, 3],
"y": [4, 5, 6],
})
source = ColumnDataSource(df)
这类写法通常很方便,但你要注意:
- DataFrame 的索引可能会被带进数据源
- 分组、聚合、重置索引之后,列名可能和你预期的不一样
- 时间索引、分层索引在转换后尤其容易让字段名变复杂
经验做法:
- 转成
ColumnDataSource前先看一眼df.columns - 必要时先
reset_index() - 在图里真正引用字段前,先检查
source.column_names
如果你后面要做分组聚合和过滤,建议同时阅读第四章。
坑 4:直接改 source.data["x"],却没有触发前端更新
在 Python 端,最稳妥的方式通常是整体替换:
source.data = {
"x": [10, 20, 30],
"y": [40, 50, 60],
}
在浏览器端 CustomJS 回调里,如果你是直接修改已有数组,通常还需要手动通知:
source.data["x"][0] = 999
source.change.emit()
可以这样理解:
- Python 端:整体赋值最清晰,Bokeh 也最容易判断发生了更新
- 浏览器端
CustomJS:如果你直接改内部数组,经常要调用source.change.emit()
这一点和第六章:交互功能场景实战里的 CustomJS 回调是连在一起的。
坑 5:把 ColumnDataSource 当成通用数据处理工具
ColumnDataSource 非常适合:
- 给 glyph 提供数据
- 给 HoverTool、选择联动、回调共享数据
- 做
stream()/patch()这类前后端同步友好的更新
但它不适合替代:
- pandas 的数据清洗
- numpy 的数值计算
- 复杂分组聚合
- 复杂业务逻辑加工
更稳妥的流程通常是:
- 先用 pandas / numpy 把数据处理好
- 再转换为
ColumnDataSource - 最后在 Bokeh 中做展示、映射和交互
一句话总结:
ColumnDataSource是可视化层的数据容器,不是完整的数据分析层。
2.4 Figure:图表的画布
Figure 是你最常接触的对象。它负责承载:
- 坐标轴
- 网格
- 标题
- 工具栏
- 图形渲染器
- 范围设置
2.4.1 创建 Figure
from bokeh.plotting import figure
p = figure(
title="销售趋势",
width=700,
height=400,
x_axis_label="月份",
y_axis_label="销售额",
tools="pan,wheel_zoom,box_zoom,reset,save",
)
2.4.2 范围控制
from bokeh.models import Range1d, DataRange1d
from bokeh.plotting import figure
# 固定范围
p = figure(x_range=Range1d(0, 10), y_range=Range1d(0, 100))
# 自动范围
p = figure(x_range=DataRange1d(), y_range=DataRange1d())
经验上可以这样选:
- 范围固定:适合你明确知道边界的图
- 自动范围:适合探索性分析或数据经常变化的图
2.4.3 Figure 和 Plot
在日常使用里,你几乎总是通过 figure() 创建图表。
它返回的是一个配置好的绘图对象,适合绝大多数场景。
你可以把 Figure 理解为:
一个“带常用配置的 Plot”,让你更方便地添加图形和交互。
2.5 Glyph:真正被画出来的东西
Glyph 表示一种图形语义,比如:
circlelinevbarpatchtext
例如:
from bokeh.plotting import figure, show
p = figure()
p.line([1, 2, 3, 4], [2, 5, 3, 6], line_width=2)
p.circle([1, 2, 3, 4], [2, 5, 3, 6], size=10)
show(p)
这段代码里其实创建了两个渲染层:
- 一层画线
- 一层画点
2.5.1 常见 Glyph 类型
| 类型 | 常见方法 | 用途 |
|---|---|---|
| 点 | circle, square, triangle | 散点、标记 |
| 线 | line, step, multi_line | 趋势、轨迹 |
| 柱 | vbar, hbar, quad | 分类比较 |
| 面 | patch, varea, harea | 区域表达 |
| 文本 | text | 标注 |
| 图像 | image, image_rgba | 栅格图像 |
2.5.2 Glyph 和 Renderer 的关系
当你写:
renderer = p.circle("x", "y", source=source, size=12)
返回值其实不是单纯的圆,而是一个 GlyphRenderer。
它把三件事绑定在一起:
- 用哪份数据:
data_source - 用哪种图形:
glyph - 用什么状态样式:选中、未选中、悬停
你可以把它理解为:
Renderer = 数据 + 图形定义 + 渲染状态
2.6 视觉属性:图是怎么“长成这样”的
Bokeh 的视觉属性大体可以分为三类:
2.6.1 填充属性
p.circle(
"x", "y", source=source,
fill_color="steelblue",
fill_alpha=0.6,
)
常见属性:
fill_colorfill_alpha
2.6.2 线条属性
p.circle(
"x", "y", source=source,
line_color="black",
line_width=2,
line_alpha=0.8,
line_dash="dashed",
)
常见属性:
line_colorline_widthline_alphaline_dash
2.6.3 文本属性
p.text(
"x", "y", text="label", source=source,
text_font_size="12pt",
text_color="black",
text_align="center",
)
常见属性:
text_fonttext_font_sizetext_colortext_aligntext_baseline
2.6.4 颜色的常见写法
fill_color="red"
fill_color="#FF5733"
fill_color=(255, 87, 51)
fill_color="color_column"
其中 "color_column" 很重要,它表示颜色来自数据列,而不是固定值。
2.7 选择、未选择与悬停状态
Bokeh 的交互并不只是“能点一下”,而是内建了不同状态的视觉反馈。
renderer = p.circle(
"x", "y", source=source, size=15,
fill_color="blue",
selection_fill_color="red",
nonselection_fill_color="gray",
nonselection_fill_alpha=0.2,
)
也可以更明确地指定不同状态的图形:
from bokeh.models import Circle
renderer.selection_glyph = Circle(fill_color="red", size=18)
renderer.nonselection_glyph = Circle(fill_color="lightgray", fill_alpha=0.2)
renderer.hover_glyph = Circle(fill_color="orange", size=18)
这一机制非常适合做:
- 多图联动
- 高亮选中点
- 弱化未选中数据
- 配合
HoverTool做分析
2.8 Document:把所有对象组织起来
2.8.1 什么是 Document
Document 是 Bokeh 的顶层容器。
一个文档里可以包含:
- 一个或多个图表
- 布局容器
- 控件
- 数据源
- 回调逻辑
from bokeh.document import Document
from bokeh.plotting import figure
doc = Document()
p = figure()
p.circle([1, 2, 3], [4, 5, 6])
doc.add_root(p)
2.8.2 为什么需要它
Document 的作用可以概括为三点:
- 组织对象关系:哪些图、哪些控件属于同一个页面
- 支持序列化:把 Python 对象转换成浏览器可理解的数据结构
- 支持同步:在 Bokeh Server 模式下,把变更推给前端
你可以把它理解成:
Document不是“图的一部分”,而是“整页 Bokeh 应用的容器”。
2.9 Bokeh 的三层使用方式
从日常开发角度,Bokeh 可以分成三层:
2.9.1 bokeh.plotting:最常用
这是你最常写的一层:
from bokeh.plotting import figure, show
p = figure()
p.line([1, 2, 3], [4, 5, 6])
show(p)
特点:
- 上手快
- 代码短
- 适合大多数常规图表
2.9.2 bokeh.models:需要精细控制时使用
from bokeh.models import Range1d, HoverTool
from bokeh.plotting import figure
p = figure(x_range=Range1d(0, 100))
p.add_tools(HoverTool(tooltips=[("x", "@x"), ("y", "@y")]))
适合:
- 精细配置轴、范围、工具、标注
- 控制数据源、选择状态、过滤器
- 处理复杂布局或交互
2.9.3 bokeh.server:需要实时交互时使用
运行方式:下面这个例子属于 Bokeh Server 应用。请将代码保存为
app.py后使用bokeh serve app.py --show运行。直接执行python app.py或用show()导出静态 HTML 时,Python 回调不会生效。更完整的运行模型、部署方式和会话机制,见第七章:Bokeh Server 进阶。
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Slider
from bokeh.plotting import figure
source = ColumnDataSource(data=dict(x=[1, 2, 3], y=[1, 4, 9]))
p = figure(title="动态示例")
p.line("x", "y", source=source, line_width=2)
slider = Slider(start=1, end=5, value=2, step=0.1, title="指数")
def update(attr, old, new):
exponent = slider.value
x = list(range(1, 20))
y = [v ** exponent for v in x]
source.data = dict(x=x, y=y)
slider.on_change("value", update)
curdoc().add_root(column(p, slider))
适合:
- 控件驱动数据更新
- 周期性推送新数据
- 多用户会话
- 真正的 Python 端回调
2.10 本章小结
这一章最重要的收获,不是记住了多少 API,而是建立了 Bokeh 的对象模型:
Figure是图表容器Glyph决定“画什么”ColumnDataSource决定“数据从哪里来”Document负责把整套对象组织起来并交给前端BokehJS在浏览器里完成渲染和交互
如果你觉得后面的交互、布局、Server 一下子变复杂了,可以先回到这一章重新确认一句话:
Bokeh 的很多高级能力,本质上都是“数据源 + 图形对象 + 文档同步”的组合。
本章和后续章节的关系
- 如果你接下来最想学“该画什么图”,读第三章:基础图表类型
- 如果你接下来最想学“怎么做颜色映射、过滤、分组和变换”,读第四章:数据转换与处理
- 如果你最关心“悬停、选择、回调、联动”,读第六章:交互功能场景实战
- 如果你最关心“为什么 Python 回调只有在服务里才生效”,读第七章:Bokeh Server 进阶
2.11 常见坑
坑 1:把 ColumnDataSource 当成“可有可无”的包装层
在简单示例里,直接传列表当然能画图。
但一旦进入:
HoverTool- 多字段 tooltip
- 颜色/大小映射
- 多图联动
CustomJS- Bokeh Server 回调
你迟早还是要回到 ColumnDataSource。
所以更实际的学习策略是:尽早习惯用它来组织数据。
坑 2:混淆“静态 HTML 交互”和“Python 回调交互”
这是 Bokeh 初学者最容易误解的一点:
show()/save()输出的 standalone HTML,可以有浏览器端交互- 但 Python 回调不会在静态 HTML 里自动运行
- 真正的 Python 回调需要
bokeh serve
如果你发现“控件能显示,但 Python 函数没执行”,优先回看本章 2.9.3 和第七章。
坑 3:只记 API,不建立对象关系
如果只记“散点图用什么函数、悬停怎么写、滑块怎么绑”,后面很容易越学越碎。
更有效的方式是始终把每个问题放回这几个核心对象里理解:
- 数据在
ColumnDataSource - 图形在 glyph / renderer
- 页面状态在
Document - 浏览器端由
BokehJS接手
这样你后面读布局、交互、输出、Server,会顺很多。
2.9.4 如何选择
可以直接按下面的原则判断:
- 先用
bokeh.plotting - 发现不够用时,再引入
bokeh.models - 需要 Python 实时回调时,再用
bokeh.server
对大多数教程代码来说,plotting + models 已经够用了。
2.10 从 Python 到浏览器:Bokeh 是怎么工作的
这是本章最重要的理解之一。
2.10.1 整体流程
Python 代码
↓
创建 Bokeh 模型对象
↓
组装成 Document
↓
序列化为 JSON
↓
交给浏览器中的 BokehJS
↓
BokehJS 创建对应视图并渲染到 Canvas
也就是说,浏览器并不直接运行你的 Python 绘图代码。
浏览器拿到的是 Bokeh 模型的描述数据,真正负责显示的是 BokehJS。
2.10.2 BokehJS 是什么
BokehJS 是运行在浏览器中的前端渲染引擎。它负责:
- 解析模型数据
- 创建前端视图对象
- 在 Canvas 上绘制图形
- 处理鼠标、键盘、触摸等事件
- 根据变更重绘图表
你可以把前后端关系理解为:
- Python 端:定义“图表是什么”
- BokehJS:负责“把它画出来并响应交互”
2.11 渲染与更新的最小心智模型
你不需要记住全部内部细节,但最好记住这条链路:
2.11.1 首次渲染
Figure / Glyph / DataSource
↓
Document
↓
JSON
↓
BokehJS
↓
Canvas 渲染
2.11.2 用户交互
例如悬停、框选、缩放:
- 浏览器端工具先响应
- 必要时更新前端状态
- 如果是 Server 应用,变更还可以同步回 Python
2.11.3 数据更新
如果你修改:
source.data = {
"x": [1, 2, 3],
"y": [2, 4, 8],
}
那么 Bokeh 会认为:
- 数据源发生变化
- 依赖这份数据的 renderer 需要更新
- 浏览器端收到变更后重绘
这就是为什么 ColumnDataSource 是 Bokeh 更新机制的核心。
2.12 一个串起全部概念的例子
下面这段代码把本章主要对象串在一起:
from bokeh.models import ColumnDataSource, HoverTool
from bokeh.plotting import figure, show
source = ColumnDataSource(data={
"x": [1, 2, 3, 4, 5],
"y": [2, 5, 8, 2, 7],
"label": ["A", "B", "C", "D", "E"],
"color": ["#4C78A8", "#F58518", "#E45756", "#72B7B2", "#54A24B"],
})
p = figure(
title="核心概念示例",
width=700,
height=400,
tools="pan,wheel_zoom,box_select,reset",
)
renderer = p.circle(
"x", "y",
source=source,
size=16,
fill_color="color",
line_color="white",
line_width=2,
selection_fill_color="red",
nonselection_fill_alpha=0.25,
)
p.add_tools(HoverTool(tooltips=[
("标签", "@label"),
("坐标", "(@x, @y)"),
]))
show(p)
这段代码里:
source提供数据figure()创建画布circle()添加 glyph,并返回 rendererHoverTool使用数据列生成提示show()触发文档输出与浏览器渲染
如果你能清楚说出这五步分别做了什么,本章的核心就已经掌握了。
2.13 本章总结
请记住下面这些结论:
1. ColumnDataSource 是 Bokeh 的标准数据中心
它让图形、悬停、选择、联动、更新都围绕同一份数据工作。
2. Figure 是画布,Glyph 是图形
一个图表通常由多个 glyph 叠加而成。
3. GlyphRenderer 负责把数据和图形绑定起来
它连接 data_source、glyph 和不同交互状态。
4. Document 是整页 Bokeh 对象的容器
它负责组织、序列化和同步。
5. 真正渲染图表的是浏览器中的 BokehJS
Python 端定义模型,BokehJS 负责显示与交互。
6. 学习路径建议
建议你按这个顺序掌握 Bokeh:
figure()和常见 glyphColumnDataSourceHoverTool、选择状态、联动bokeh.models的精细控制Bokeh Server
2.14 下一章预告
理解了本章以后,你已经掌握了 Bokeh 的最小核心模型。
下一章可以进一步学习:
- 常见图表类型如何组织
- 不同 glyph 的使用场景
- 如何从“会画图”过渡到“会设计图表”
继续阅读:第三章:常见图表类型