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

第五章:布局系统与样式定制

前置知识:本章需要掌握基础图表绘制(第三章)和 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 是对当前 Bokeh Document 生效的全局主题设置。在 Bokeh Server 中最直观;如果你只是生成单个 standalone HTML,也可以理解为“给当前输出文档设置统一样式”。如果你只想改一个图,不一定要上升到主题层,直接设置 p.titlep.axisp.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_widthstretch_both
  • 优先让“外层布局控制伸缩,内层图表只设置必要高度”

如果你对尺寸控制还不熟,可以结合第八章一起看,因为不同输出环境对响应式布局的表现也会有差异。

坑 2:主题没有生效,或者效果和预期不一样

常见原因包括:

  • 你修改的是单个图表属性,但同时又设置了全局主题
  • 你在不同 Document 上设置了主题
  • 你本来只想局部微调,却用了全局主题

经验上可以这样选:

  • 统一页面风格:优先用主题
  • 只改一个图的局部样式:优先直接设置属性
  • 需要动态切换主题:更适合在 Bokeh Server 应用里做

坑 3:图例、工具栏、控件一多,页面马上显得拥挤

这通常不是某个 API 的问题,而是信息层级没有整理好。可以考虑:

  • 把次要图放到 Tabs
  • 合并多个图的工具栏
  • 用交互筛选替代一次性展示全部内容
  • 把“说明性文字”放到图外,而不是全塞进标题和图例

这类问题会在第六章里变得更明显,因为交互控件一多,布局设计就会直接影响可用性。

坑 4:布局已经复杂到像应用,但代码仍然像一次性脚本

当你开始同时管理多个图、多个控件、共享数据源和主题时,代码就已经接近“小型应用”了。这时建议:

  • 把“数据准备”“图表创建”“布局组织”拆成不同函数
  • 提前统一颜色、字体、图例位置等样式约定
  • 明确哪些组件共享数据源,哪些只是并排展示

如果你的页面已经需要 Python 回调、状态同步和周期更新,说明你很可能应该转到第七章:Bokeh Server 进阶