第五章:布局系统与样式定制
前置知识:本章需要掌握基础图表绘制(第三章)和 ColumnDataSource(第二章 2.2)。
运行方式说明:本章大多数示例既可以在普通 Python 脚本中配合
show()运行,也可以在 Notebook 中运行;涉及curdoc().theme的示例更适合放在 Bokeh Server 或当前活动 Document 的上下文中理解。关于运行模式的整体区别,可先回看第一章 1.5和第八章 8.1。延伸阅读:布局系统是构建交互式仪表板的基础,结合第六章的交互功能可以创建完整的分析应用。主题和样式的更多参考见附录。
5.1 为什么需要布局系统?
场景:你想在一个页面展示4个图表。
直觉做法:创建4个HTML文件,手动拼接。
Bokeh方式:使用布局函数
from bokeh.layouts import row, column, gridplot
layout = gridplot([[p1, p2], [p3, p4]])
show(layout)
5.2 基础布局
row - 水平排列:
from bokeh.layouts import row
layout = row(p1, p2, p3) # 水平排列
layout = row(p1, p2, sizing_mode="stretch_width") # 自适应宽度
column - 垂直排列:
from bokeh.layouts import column
layout = column(p1, p2, p3) # 垂直排列
layout = column(p1, p2, width=800) # 固定宽度
嵌套布局:
# 复杂布局
layout = column(
row(p1, p2),
row(p3, p4)
)
5.3 网格布局
gridplot - 网格排列:
from bokeh.layouts import gridplot
# 2x2网格
grid = gridplot([[p1, p2], [p3, p4]])
# 自动填充None
grid = gridplot([[p1, p2], [p3, None]]) # 右下角为空
# 统一工具栏
grid = gridplot([[p1, p2], [p3, p4]], toolbar_location='right')
# 合并工具栏
grid = gridplot([[p1, p2], [p3, p4]], merge_tools=True)
响应式网格:
grid = gridplot([[p1, p2], [p3, p4]],
sizing_mode="stretch_both", # 拉伸填充
toolbar_location=None,
)
5.4 标签页布局
from bokeh.models import Tabs, Panel
tab1 = Panel(child=p1, title="图表1")
tab2 = Panel(child=p2, title="图表2")
tab3 = Panel(child=p3, title="图表3")
tabs = Tabs(tabs=[tab1, tab2, tab3])
show(tabs)
5.5 样式定制
主题:
说明:
curdoc().theme是对当前 BokehDocument生效的全局主题设置。在 Bokeh Server 中最直观;如果你只是生成单个 standalone HTML,也可以理解为“给当前输出文档设置统一样式”。如果你只想改一个图,不一定要上升到主题层,直接设置p.title、p.axis、p.grid等属性通常更简单。输出方式的区别可参考第八章。
from bokeh.themes import Theme
from bokeh.io import curdoc
# 内置主题
curdoc().theme = "dark_minimal"
curdoc().theme = "caliber"
curdoc().theme = "night_sky"
# 自定义主题
theme = Theme(json={
'attrs': {
'Figure': {
'background_fill_color': '#f5f5f5',
'outline_line_color': 'gray',
},
'Axis': {
'axis_line_color': 'gray',
'major_tick_line_color': 'gray',
}
}
})
curdoc().theme = theme
标题样式:
p.title.text = "图表标题"
p.title.text_font_size = "20pt"
p.title.text_color = "navy"
p.title.align = "center"
p.title.background_fill_color = "#f0f0f0"
坐标轴样式:
# 轴标签
p.xaxis.axis_label = "X轴"
p.yaxis.axis_label = "Y轴"
p.xaxis.axis_label_text_font_size = "14pt"
# 刻度
p.xaxis.major_tick_line_color = "red"
p.xaxis.minor_tick_line_color = "gray"
p.xaxis.major_label_text_font_size = "10pt"
# 格式化
from bokeh.models import NumeralTickFormatter
p.yaxis.formatter = NumeralTickFormatter(format="0,0") # 千分位
p.yaxis.formatter = NumeralTickFormatter(format="$0.00") # 货币
网格线样式:
p.grid.grid_line_color = "gray"
p.grid.grid_line_alpha = 0.3
p.grid.grid_line_dash = "dashed"
# 只显示Y轴网格
p.xgrid.grid_line_color = None
图例样式:
p.legend.location = "top_right"
p.legend.orientation = "vertical" # 或 "horizontal"
p.legend.background_fill_color = "#f0f0f0"
p.legend.background_fill_alpha = 0.8
p.legend.border_line_color = "gray"
p.legend.label_text_font_size = "12pt"
# 图例交互
p.legend.click_policy = "hide" # 点击隐藏
p.legend.click_policy = "mute" # 点击静音(变淡)
工具栏样式:
p.toolbar_location = "above" # above, below, left, right, None
p.toolbar.logo = None # 隐藏logo
p.toolbar.autohide = True # 自动隐藏
5.6 响应式布局
使用 sizing_mode:
from bokeh.layouts import row
from bokeh.plotting import figure
p1 = figure(sizing_mode="stretch_width", height=300)
p2 = figure(sizing_mode="stretch_width", height=300)
layout = row(p1, p2, sizing_mode="stretch_width")
# sizing_mode 选项:
# - 'fixed':固定尺寸
# - 'stretch_width':拉伸宽度
# - 'stretch_height':拉伸高度
# - 'stretch_both':拉伸宽度和高度
# - 'scale_width':按比例缩放宽度
# - 'scale_height':按比例缩放高度
# - 'scale_both':按比例缩放宽度和高度
5.7 高级主题定制
创建自定义主题文件:
attrs:
Figure:
background_fill_color: '#f5f5f5'
outline_line_color: null
toolbar_location: above
Axis:
axis_line_color: '#DDDDDD'
major_tick_line_color: '#DDDDDD'
minor_tick_line_color: null
axis_label_text_font_size: '12pt'
major_label_text_font_size: '10pt'
Grid:
grid_line_color: '#EEEEEE'
Legend:
background_fill_color: '#FFFFFF'
border_line_color: '#DDDDDD'
Title:
text_font_size: '16pt'
text_color: '#333333'
应用主题文件:
from bokeh.themes import Theme
from bokeh.io import curdoc
theme = Theme(filename='theme.yaml')
curdoc().theme = theme
动态切换主题:
from bokeh.models import Select
def change_theme(attr, old, new):
curdoc().theme = new
theme_select = Select(
title="主题",
options=["caliber", "dark_minimal", "night_sky", "contrast"],
value="caliber"
)
theme_select.on_change('value', change_theme)
5.8 本章小结
本章你已经掌握了 Bokeh 中把多个图和控件组织成一个页面的基本方法:
- 知道何时使用
row()、column()、gridplot() - 了解了标签页布局的基本写法
- 学会通过标题、坐标轴、网格、图例、工具栏来调整视觉样式
- 初步理解了
sizing_mode对响应式布局的重要性 - 了解了主题适合“统一风格”,逐属性设置适合“局部精调”
如果你下一步想让页面“动起来”,建议继续阅读第六章:交互功能场景实战;如果你想把这些布局输出到 HTML、Notebook 或嵌入现有网页,可以接着看第八章:输出选项。
5.9 常见坑
坑 1:布局能显示,但尺寸很奇怪
最常见原因是图表本身和外层布局同时设置了不同的 sizing_mode,导致拉伸策略冲突。排查时建议:
- 先全部用固定宽高验证布局结构
- 再逐步引入
stretch_width或stretch_both - 优先让“外层布局控制伸缩,内层图表只设置必要高度”
如果你对尺寸控制还不熟,可以结合第八章一起看,因为不同输出环境对响应式布局的表现也会有差异。
坑 2:主题没有生效,或者效果和预期不一样
常见原因包括:
- 你修改的是单个图表属性,但同时又设置了全局主题
- 你在不同
Document上设置了主题 - 你本来只想局部微调,却用了全局主题
经验上可以这样选:
- 统一页面风格:优先用主题
- 只改一个图的局部样式:优先直接设置属性
- 需要动态切换主题:更适合在 Bokeh Server 应用里做
坑 3:图例、工具栏、控件一多,页面马上显得拥挤
这通常不是某个 API 的问题,而是信息层级没有整理好。可以考虑:
- 把次要图放到
Tabs - 合并多个图的工具栏
- 用交互筛选替代一次性展示全部内容
- 把“说明性文字”放到图外,而不是全塞进标题和图例
这类问题会在第六章里变得更明显,因为交互控件一多,布局设计就会直接影响可用性。
坑 4:布局已经复杂到像应用,但代码仍然像一次性脚本
当你开始同时管理多个图、多个控件、共享数据源和主题时,代码就已经接近“小型应用”了。这时建议:
- 把“数据准备”“图表创建”“布局组织”拆成不同函数
- 提前统一颜色、字体、图例位置等样式约定
- 明确哪些组件共享数据源,哪些只是并排展示
如果你的页面已经需要 Python 回调、状态同步和周期更新,说明你很可能应该转到第七章:Bokeh Server 进阶。