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

第八章:输出选项

前置知识:本章需要了解基础图表绘制(第一章)和 Figure 对象(第二章 2.3)。

Bokeh提供了多种输出方式,让你的可视化作品能够在不同场景中展示和使用。本章将详细介绍各种输出选项的特点、配置方法和适用场景。

在进入具体 API 之前,先抓住一个总原则:

  • standalone 文档:不需要 Bokeh Server,适合 output_file()save()file_html()components()、Notebook 内联展示
  • Bokeh Server 应用:需要 bokeh serve 运行,适合 server_document()server_session() 这类“嵌入一个活的 Python 应用”的场景
  • 静态导出:PNG / SVG 适合报告、论文、幻灯片,但会失去 Python 端交互能力

如果你还不熟悉 Bokeh 的运行模式,建议先回看第一章 1.5第七章

8.1 输出格式概览

8.1.1 先按“运行模式”理解输出

运行模式是否需要 Bokeh Server常见 API适用场景
Standalone HTMLoutput_file()save()file_html()分享单图、保存本地 HTML、导出离线文档
Notebook 内联output_notebook()show()Jupyter 教学、探索式分析
Standalone 嵌入components()json_item()嵌入 Flask / Django / 任意网页模板
Server 嵌入server_document()server_session()嵌入需要 Python 回调的交互应用
静态图片否(但需要浏览器驱动)export_png()export_svg()论文、汇报、印刷、快照导出

8.1.2 格式对比

输出格式适用场景优点缺点
HTML独立分享、Web嵌入交互完整、易于分享需要浏览器、文件较大
PNG报告、演示文稿通用性强、易于查看静态图片、失去交互性
SVG出版印刷、矢量编辑无损缩放、可编辑部分交互丢失
Notebook数据分析、教学内联展示、便于记录依赖Jupyter环境
组件Web应用集成灵活嵌入、可定制需要Web开发知识
Server 应用嵌入Web 应用集成保留 Python 回调、状态同步需要 Bokeh Server 进程

8.1.3 选择决策指南

需要输出图表?
    │
    ├── 只是想保存或分享一份交互式结果?
    │   └── standalone HTML:output_file / save / file_html
    │
    ├── 想在 Jupyter 中直接显示?
    │   └── output_notebook + show
    │
    ├── 想嵌入现有网页?
    │   ├── 不需要 Python 回调 → components / json_item
    │   └── 需要 Python 回调 → server_document / server_session
    │
    └── 需要静态图片?
        ├── 屏幕展示 / 报告 → PNG
        └── 印刷 / 矢量编辑 → SVG

8.1.4 components()file_html()server_document() 怎么选

这是最容易混淆的三个接口,可以先这样记:

接口是否需要 Bokeh Server返回内容适合什么
file_html()一整份 HTML 字符串想自己生成完整 HTML 页面
components()<script> + <div> 片段想把 standalone 图表嵌入模板
server_document()指向 Bokeh Server 应用的 <script>想把“活的” Bokeh Server 应用嵌入网页

你可以把它们理解成:

  • file_html()整页导出
  • components()页面局部嵌入
  • server_document()嵌入远端运行中的 Server 应用

后面在 8.6 节会分别展开。

8.2 HTML 输出

HTML 是最常用的输出格式,生成的文件包含完整的浏览器端交互功能。

运行方式说明:本节讨论的 output_file()save()file_html() 都属于 standalone 输出。它们不需要 bokeh serve,但也不能承载 Python 回调。若你需要 Python 回调,请跳到第七章和本章 8.6 节的 server_document()

基础用法

from bokeh.plotting import figure, output_file, save

# 创建图表
p = figure(title="销售趋势", width=800, height=400)
p.line([1, 2, 3, 4, 5], [100, 150, 120, 180, 200], line_width=2)
p.circle([1, 2, 3, 4, 5], [100, 150, 120, 180, 200], size=8)

# 输出为HTML文件
output_file("sales_chart.html")
save(p)

高级配置

from bokeh.plotting import figure, output_file, save
from bokeh.resources import CDN, INLINE

# 使用CDN资源(推荐,文件更小)
output_file("chart_cdn.html", 
    title="我的图表",
    mode="cdn"  # 使用CDN加载BokehJS
)

# 使用内联资源(自包含文件)
output_file("chart_inline.html",
    title="我的图表", 
    mode="inline"  # 将BokehJS内联到HTML中
)

# 自定义资源
from bokeh.resources import Resources
custom_resources = Resources(mode="server", root_url="http://myserver.com/static")
output_file("chart_custom.html", resources=custom_resources)

自包含HTML文件

当你需要生成一个完全自包含的HTML文件时(比如通过邮件发送),使用内联模式:

from bokeh.plotting import figure, output_file, save
from bokeh.resources import INLINE

# 创建图表
p = figure(title="自包含图表")
p.circle([1, 2, 3], [4, 5, 6])

# 生成自包含HTML(所有JavaScript都内联在文件中)
output_file("self_contained.html", mode="inline")
save(p)

# 文件可以离线查看,但文件较大

多图表输出

from bokeh.plotting import figure, output_file, save
from bokeh.layouts import column, row

# 创建多个图表
p1 = figure(title="图表1", width=400, height=300)
p1.circle([1, 2, 3], [4, 5, 6])

p2 = figure(title="图表2", width=400, height=300)
p2.line([1, 2, 3], [6, 5, 4])

p3 = figure(title="图表3", width=400, height=300)
p3.vbar(x=[1, 2, 3], top=[4, 5, 6], width=0.5)

# 布局并输出
layout = row(column(p1, p2), p3)
output_file("multiple_charts.html")
save(layout)

8.3 PNG 导出

PNG导出需要额外的依赖,适合生成静态报告图片。

依赖安装

方式1:使用 conda(推荐)

# Firefox 路线:安装 Selenium、Firefox 和 geckodriver
conda install selenium firefox geckodriver -c conda-forge

# Chrome 路线:安装 Selenium 和 ChromeDriver
# 注意:ChromeDriver 版本必须与本机 Chrome/Chromium 主版本匹配
conda install selenium python-chromedriver-binary -c conda-forge

方式2:使用pip

# 安装Selenium
pip install selenium

# 下载并配置WebDriver
# Firefox: 下载geckodriver并添加到PATH
# Chrome: 下载ChromeDriver并添加到PATH

基础导出

from bokeh.plotting import figure
from bokeh.io import export_png

# 创建图表
p = figure(title="导出示例", width=800, height=400)
p.line([1, 2, 3, 4, 5], [2, 5, 8, 2, 7], line_width=2)

# 导出为PNG
export_png(p, filename="chart.png")

# 默认分辨率:屏幕分辨率
# 可以通过webdriver参数指定使用Chrome或Firefox
from selenium import webdriver
driver = webdriver.Chrome()  # 或 webdriver.Firefox()
export_png(p, filename="chart.png", webdriver=driver)
driver.quit()

高分辨率导出

from bokeh.plotting import figure
from bokeh.io import export_png
from bokeh.models import Plot
from bokeh.layouts import column

# 创建高分辨率图表
p = figure(title="高分辨率图表", 
    width=1600,  # 2倍宽度
    height=800,  # 2倍高度
    sizing_mode="scale_width"
)

# 调整字体大小以适应高分辨率
p.title.text_font_size = "24pt"
p.xaxis.major_label_text_font_size = "14pt"
p.yaxis.major_label_text_font_size = "14pt"

p.line([1, 2, 3, 4, 5], [2, 5, 8, 2, 7], line_width=4)

# 导出
export_png(p, filename="high_res_chart.png")

批量导出

from bokeh.plotting import figure
from bokeh.io import export_png
import os

# 确保输出目录存在
os.makedirs("charts", exist_ok=True)

# 创建多个图表并导出
datasets = [
    ("sales", [100, 150, 120, 180, 200]),
    ("profit", [20, 30, 25, 40, 35]),
    ("customers", [50, 60, 55, 70, 65])
]

for name, data in datasets:
    p = figure(title=f"{name.title()} 数据", width=800, height=400)
    p.line(range(len(data)), data, line_width=2)
    p.circle(range(len(data)), data, size=8)
    
    # 导出到charts目录
    export_png(p, filename=f"charts/{name}_chart.png")
    print(f"已导出: charts/{name}_chart.png")

print("所有图表导出完成!")

8.4 SVG 导出

SVG是矢量格式,适合出版印刷和进一步编辑。

基础导出

from bokeh.plotting import figure
from bokeh.io import export_svg

# 创建图表,指定SVG后端
p = figure(title="SVG导出", width=800, height=400,
    output_backend="svg")  # 重要:指定SVG渲染

p.circle([1, 2, 3, 4, 5], [2, 5, 8, 2, 7], size=10, color="navy")
p.line([1, 2, 3, 4, 5], [2, 5, 8, 2, 7], line_width=2)

# 导出SVG
export_svg(p, filename="chart.svg")

SVG vs Canvas 渲染

from bokeh.plotting import figure, show

# Canvas渲染(默认,适合大数据集)
p_canvas = figure(title="Canvas渲染", output_backend="canvas")
p_canvas.circle(range(10000), [i**0.5 for i in range(10000)], size=1)

# SVG渲染(适合出版,支持矢量编辑)
p_svg = figure(title="SVG渲染", output_backend="svg")
p_svg.circle([1, 2, 3], [4, 5, 6], size=10)

# 根据需求选择渲染后端
# 大数据集:使用canvas
# 需要导出SVG:使用svg
# 需要最佳性能:使用webgl

8.5 Jupyter Notebook 输出

Bokeh 与 Jupyter Notebook / JupyterLab 可以很好地集成,支持内联交互式图表。

交叉引用:如果你只想快速在 Notebook 里显示图表,可先看第一章 1.5;如果你需要排查 JupyterLab 显示问题,可结合常见问题与故障排查一起看。

经典 Jupyter Notebook

from bokeh.plotting import figure, show
from bokeh.io import output_notebook

# 初始化Notebook输出
output_notebook()

# 创建图表
p = figure(title="Notebook内联图表", width=600, height=400)
p.circle([1, 2, 3, 4, 5], [2, 5, 8, 2, 7], size=10)

# 显示图表(直接在Notebook中显示)
show(p)

JupyterLab 配置

安装扩展:

# 使用pip
pip install jupyter_bokeh

# 使用conda
conda install jupyter_bokeh -c conda-forge

使用方法:

from bokeh.plotting import figure, show
from bokeh.io import output_notebook

# JupyterLab中同样使用output_notebook
output_notebook()

# 创建图表
p = figure(title="JupyterLab图表", width=600, height=400)
p.line([1, 2, 3], [4, 5, 6])

show(p)

Notebook 高级技巧

from bokeh.plotting import figure, show
from bokeh.io import output_notebook
from bokeh.layouts import column, row
from bokeh.models import Slider, CustomJS

output_notebook()

# 在一个单元格中显示多个图表
p1 = figure(title="图表1", width=300, height=200)
p1.circle([1, 2, 3], [4, 5, 6])

p2 = figure(title="图表2", width=300, height=200)
p2.line([1, 2, 3], [6, 5, 4])

# 水平排列显示
show(row(p1, p2))

# 在Notebook中使用交互控件
slider = Slider(start=0, end=10, value=1, title="数值")
slider.js_on_change('value', CustomJS(code="""
    console.log('滑块值变为: ' + this.value)
"""))

show(slider)

Notebook 常见问题

问题1:图表不显示

# 解决方案:确保调用output_notebook()
from bokeh.io import output_notebook
output_notebook()  # 必须在show()之前调用

问题2:JupyterLab中无响应

# 解决方案:安装jupyter_bokeh扩展
pip install jupyter_bokeh
# 然后重启JupyterLab

8.6 嵌入 Web 框架

Bokeh 可以轻松嵌入到 Flask、Django 等 Web 框架中。但在写代码之前,必须先分清两类嵌入:

  1. 嵌入 standalone 文档
    • 不需要 Bokeh Server
    • 适合 components()json_item()file_html()
    • 只能使用浏览器端交互和 CustomJS
  2. 嵌入 Bokeh Server 应用
    • 需要单独运行 bokeh serve
    • 适合 server_document()server_session()
    • 支持 Python 回调、会话状态和实时更新

如果你不确定该选哪种,优先问自己一句:这张图是否需要 Python 在后台持续参与交互?

  • 如果答案是否定的,通常用 components() 就够了
  • 如果答案是肯定的,就应该考虑 server_document() + Bokeh Server

8.6.1 使用 components() 生成 standalone 组件

from bokeh.plotting import figure
from bokeh.embed import components

# 创建图表
p = figure(title="嵌入示例", width=600, height=400)
p.circle([1, 2, 3, 4, 5], [2, 5, 8, 2, 7])

# 生成脚本和div标签
script, div = components(p)

print("JavaScript脚本:")
print(script[:200] + "...")  # 脚本很长,只显示开头

print("\nHTML div:")
print(div)

# 返回的是元组 (script, div)
# script: 包含所有必要的JavaScript代码
# div: 包含图表的HTML容器

适用判断:

  • 你已经有一个 Flask / Django / FastAPI 模板页面
  • 你只想把一张或几张 Bokeh 图表嵌进去
  • 这些图表不依赖 Python 回调持续运行

如果你需要嵌入的是 Bokeh Server 应用,请直接看后面的 8.6.4 节。

8.6.2 嵌入 Flask 应用

app.py:

from flask import Flask, render_template
from bokeh.plotting import figure
from bokeh.embed import components

app = Flask(__name__)

@app.route('/')
def index():
    # 创建图表
    p = figure(title="Flask中的Bokeh图表", width=800, height=400)
    p.line([1, 2, 3, 4, 5], [2, 5, 8, 2, 7], line_width=2)
    
    # 生成组件
    script, div = components(p)
    
    return render_template('index.html', script=script, div=div)

if __name__ == '__main__':
    app.run(debug=True)

templates/index.html:

<!DOCTYPE html>
<html>
<head>
    <title>Bokeh Flask示例</title>
    <link rel="stylesheet" href="https://cdn.bokeh.org/bokeh/release/bokeh-3.x.x.min.css">
</head>
<body>
    <h1>销售数据图表</h1>
    
    <!-- 图表容器 -->
    {{ div | safe }}
    
    <!-- Bokeh脚本 -->
    {{ script | safe }}
</body>
</html>

8.6.3 嵌入 Django 应用

views.py:

from django.shortcuts import render
from bokeh.plotting import figure
from bokeh.embed import components

def chart_view(request):
    # 创建图表
    p = figure(title="Django中的Bokeh图表", width=800, height=400)
    p.circle([1, 2, 3, 4, 5], [2, 5, 8, 2, 7], size=10)
    
    # 生成组件
    script, div = components(p)
    
    context = {
        'script': script,
        'div': div
    }
    return render(request, 'chart.html', context)

templates/chart.html:

{% load static %}
<!DOCTYPE html>
<html>
<head>
    <title>Bokeh Django示例</title>
    <link rel="stylesheet" href="https://cdn.bokeh.org/bokeh/release/bokeh-3.x.x.min.css">
</head>
<body>
    {{ div|safe }}
    {{ script|safe }}
</body>
</html>

8.6.4 嵌入 Bokeh Server 应用

当你需要把 支持 Python 回调 的 Bokeh 应用嵌入现有网站时,就不能再用 components()。这时应该先运行 Bokeh Server,再通过 server_document() 获取嵌入脚本。

from flask import Flask, render_template
from bokeh.embed import server_document

app = Flask(__name__)

@app.route('/')
def index():
    # 获取 Bokeh Server 应用的嵌入脚本
    # 假设 Bokeh Server 运行在 http://localhost:5006/myapp
    script = server_document('http://localhost:5006/myapp')
    return render_template('index.html', script=script)

这里要注意两点:

  1. server_document() 嵌入的是一个 正在运行的 Bokeh Server 应用
  2. 页面里的交互会通过 WebSocket 与服务器上的 Python 代码通信

因此,components()server_document() 的根本区别不是“返回值长什么样”,而是:

  • components() 嵌入的是 静态生成的 standalone 文档
  • server_document() 嵌入的是 动态运行的 Server 应用

如果你对 Bokeh Server 还不熟,建议先回看第七章

8.7 批量导出与自动化

自动化导出脚本

"""
批量导出图表脚本
用于生成报告所需的图表图片
"""
import os
from datetime import datetime
from bokeh.plotting import figure
from bokeh.io import export_png, export_svg
from bokeh.layouts import column, row

def create_sales_chart(data):
    """创建销售图表"""
    p = figure(title="销售趋势", width=800, height=400)
    p.line(range(len(data)), data, line_width=2, color="navy")
    p.circle(range(len(data)), data, size=8, color="navy")
    p.xaxis.axis_label = "月份"
    p.yaxis.axis_label = "销售额"
    return p

def create_comparison_chart(data1, data2):
    """创建对比图表"""
    p = figure(title="销售对比", width=800, height=400)
    p.line(range(len(data1)), data1, line_width=2, color="blue", legend_label="产品A")
    p.line(range(len(data2)), data2, line_width=2, color="red", legend_label="产品B")
    p.legend.location = "top_left"
    return p

def export_charts(output_dir="exported_charts", formats=["png", "svg"]):
    """导出所有图表"""
    # 创建输出目录
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    output_path = os.path.join(output_dir, timestamp)
    os.makedirs(output_path, exist_ok=True)
    
    # 示例数据
    sales_data = [100, 150, 120, 180, 200, 175, 220]
    product_a = [100, 120, 140, 160, 180]
    product_b = [80, 100, 130, 150, 170]
    
    # 创建图表
    charts = {
        "sales_trend": create_sales_chart(sales_data),
        "comparison": create_comparison_chart(product_a, product_b)
    }
    
    # 导出图表
    for name, chart in charts.items():
        if "png" in formats:
            png_path = os.path.join(output_path, f"{name}.png")
            export_png(chart, filename=png_path)
            print(f"已导出PNG: {png_path}")
        
        if "svg" in formats:
            chart.output_backend = "svg"  # 设置SVG渲染
            svg_path = os.path.join(output_path, f"{name}.svg")
            export_svg(chart, filename=svg_path)
            print(f"已导出SVG: {svg_path}")
    
    print(f"\n所有图表已导出到: {output_path}")
    return output_path

if __name__ == "__main__":
    export_charts()

定时导出任务

"""
定时导出图表任务
可以配合cron或Windows任务计划使用
"""
import schedule
import time
from datetime import datetime
from export_script import export_charts

def scheduled_export():
    """定时导出任务"""
    print(f"开始定时导出任务: {datetime.now()}")
    try:
        output_path = export_charts()
        print(f"导出成功: {output_path}")
    except Exception as e:
        print(f"导出失败: {e}")

# 每天早上8点执行
schedule.every().day.at("08:00").do(scheduled_export)

# 或者每小时执行
schedule.every().hour.do(scheduled_export)

print("定时任务已启动...")
while True:
    schedule.run_pending()
    time.sleep(60)

8.8 版本兼容性说明

Bokeh 版本兼容性

Bokeh版本HTMLPNGSVGJupyterLab备注
3.x当前稳定版本
2.x⚠️需要旧版扩展
1.x⚠️部分功能有限

依赖库版本要求

# requirements.txt 示例
bokeh>=3.0.0
selenium>=4.0.0
jupyter_bokeh>=3.0.0  # JupyterLab需要

# WebDriver版本需要与浏览器匹配
# Chrome: 查看 https://chromedriver.chromium.org/
# Firefox: 查看 https://github.com/mozilla/geckodriver/releases

常见兼容性问题

问题1:导出 PNG 时 WebDriver 不可用或版本不匹配

常见错误信息:

RuntimeError: Neither firefox and geckodriver nor chrome and chromedriver are available
WebDriverException: Message: 'chromedriver' executable needs to be in PATH

推荐优先使用 conda-forge 安装一组匹配的浏览器与驱动:

# Firefox 路线
conda install selenium firefox geckodriver -c conda-forge

# Chrome 路线
conda install selenium python-chromedriver-binary -c conda-forge

如果你手动安装 ChromeDriver,需要确保 ChromeDriver 与 Chrome/Chromium 的主版本匹配,并把驱动放到 PATH 中。使用 Selenium 4 手动指定驱动路径时,应使用 Service

from selenium import webdriver
from selenium.webdriver.chrome.service import Service

driver = webdriver.Chrome(service=Service("/path/to/chromedriver"))

Bokeh 的 export_png() / export_svg() 通常不需要你自己创建 webdriver 对象;上面的 Selenium 代码只用于解释现代 Selenium 4 的驱动路径写法。

问题2:JupyterLab 中图表不显示

# 安装或升级 jupyter_bokeh
pip install --upgrade jupyter_bokeh

# JupyterLab 4.x 建议使用 jupyter_bokeh 4.x 或更新版本
pip install --upgrade "jupyter_bokeh>=4.0"

安装或升级后,重启 JupyterLab 再运行:

from bokeh.io import output_notebook, show
from bokeh.plotting import figure

output_notebook()

p = figure()
p.scatter([1, 2, 3], [4, 5, 6])
show(p)

JupyterLab 3/4 通常不需要手动执行 jupyter lab build。只有在旧版 JupyterLab 或本地扩展构建失败时,才考虑清理并重建。


8.9 本章小结

本章你应该掌握四件事:

  1. 先分运行模式,再选输出接口
    • standalone:output_file()save()file_html()components()
    • Notebook:output_notebook() + show()
    • Server:server_document() / server_session() + bokeh serve
  2. components()file_html()server_document() 并不是同一类东西
    • file_html() 生成完整页面
    • components() 返回可嵌入片段
    • server_document() 嵌入的是活的 Server 应用
  3. PNG / SVG 导出是额外能力,不是默认内置能力
    • 需要 Selenium
    • 需要浏览器和对应驱动
  4. 是否需要 Python 回调,是决定输出方案的关键分界线

8.10 常见坑

  • 把 standalone 文档和 Server 应用混为一谈save() / show() 生成的 HTML 不能执行 Python 回调。
  • 误把 components() 当成 Server 嵌入方案:它只能嵌入 standalone 文档。
  • 导出 PNG/SVG 时忽略浏览器驱动依赖:先确认 Selenium、浏览器、驱动三者都可用。
  • 在 Jupyter 中只执行 show(),没先 output_notebook():这会导致图表不显示或行为异常。
  • 一上来就追求“所有格式都支持”:先明确你的交付目标,是分享交互图、嵌入网页,还是导出静态图片。

下一步:了解了输出选项后,下一章我们将学习性能优化,重点是如何判断瓶颈、选择降采样/聚合/WebGL 等策略,而不是把所有性能问题都归结为渲染后端。

延伸阅读