Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第二章:核心概念

前置知识:建议你先完成第一章

延伸阅读

本章只回答四个问题:

  1. Bokeh 为什么强调 ColumnDataSource
  2. FigureGlyphDocument 分别是什么
  3. Python 代码为什么能在浏览器里变成交互图表
  4. 选择、悬停、更新这些交互是怎么工作的

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"],
}

你可以把它当成一张表:

xylabel
14A
25B
36C

重要约束:每一列长度必须一致。
因为第 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 的数值计算
  • 复杂分组聚合
  • 复杂业务逻辑加工

更稳妥的流程通常是:

  1. 先用 pandas / numpy 把数据处理好
  2. 再转换为 ColumnDataSource
  3. 最后在 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 FigurePlot

在日常使用里,你几乎总是通过 figure() 创建图表。
它返回的是一个配置好的绘图对象,适合绝大多数场景。

你可以把 Figure 理解为:

一个“带常用配置的 Plot”,让你更方便地添加图形和交互。


2.5 Glyph:真正被画出来的东西

Glyph 表示一种图形语义,比如:

  • circle
  • line
  • vbar
  • patch
  • text

例如:

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
它把三件事绑定在一起:

  1. 用哪份数据:data_source
  2. 用哪种图形:glyph
  3. 用什么状态样式:选中、未选中、悬停

你可以把它理解为:

Renderer = 数据 + 图形定义 + 渲染状态


2.6 视觉属性:图是怎么“长成这样”的

Bokeh 的视觉属性大体可以分为三类:

2.6.1 填充属性

p.circle(
    "x", "y", source=source,
    fill_color="steelblue",
    fill_alpha=0.6,
)

常见属性:

  • fill_color
  • fill_alpha

2.6.2 线条属性

p.circle(
    "x", "y", source=source,
    line_color="black",
    line_width=2,
    line_alpha=0.8,
    line_dash="dashed",
)

常见属性:

  • line_color
  • line_width
  • line_alpha
  • line_dash

2.6.3 文本属性

p.text(
    "x", "y", text="label", source=source,
    text_font_size="12pt",
    text_color="black",
    text_align="center",
)

常见属性:

  • text_font
  • text_font_size
  • text_color
  • text_align
  • text_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 的作用可以概括为三点:

  1. 组织对象关系:哪些图、哪些控件属于同一个页面
  2. 支持序列化:把 Python 对象转换成浏览器可理解的数据结构
  3. 支持同步:在 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 的很多高级能力,本质上都是“数据源 + 图形对象 + 文档同步”的组合。

本章和后续章节的关系

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 会认为:

  1. 数据源发生变化
  2. 依赖这份数据的 renderer 需要更新
  3. 浏览器端收到变更后重绘

这就是为什么 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,并返回 renderer
  • HoverTool 使用数据列生成提示
  • show() 触发文档输出与浏览器渲染

如果你能清楚说出这五步分别做了什么,本章的核心就已经掌握了。


2.13 本章总结

请记住下面这些结论:

1. ColumnDataSource 是 Bokeh 的标准数据中心

它让图形、悬停、选择、联动、更新都围绕同一份数据工作。

2. Figure 是画布,Glyph 是图形

一个图表通常由多个 glyph 叠加而成。

3. GlyphRenderer 负责把数据和图形绑定起来

它连接 data_sourceglyph 和不同交互状态。

4. Document 是整页 Bokeh 对象的容器

它负责组织、序列化和同步。

5. 真正渲染图表的是浏览器中的 BokehJS

Python 端定义模型,BokehJS 负责显示与交互。

6. 学习路径建议

建议你按这个顺序掌握 Bokeh:

  1. figure() 和常见 glyph
  2. ColumnDataSource
  3. HoverTool、选择状态、联动
  4. bokeh.models 的精细控制
  5. Bokeh Server

2.14 下一章预告

理解了本章以后,你已经掌握了 Bokeh 的最小核心模型。
下一章可以进一步学习:

  • 常见图表类型如何组织
  • 不同 glyph 的使用场景
  • 如何从“会画图”过渡到“会设计图表”

继续阅读:第三章:常见图表类型