- 数据可视化
- 数据分析
【免费下载链接】plotly.py
The interactive graphing library for Python :sparkles:
本篇技术指南聚焦 Plotly 的 figure factory 模块在子图(subplot)场景下的使用方法。由于图工厂函数(如create_quiver、create_streamline、create_table)返回的是完整独立的 Figure 对象,无法像普通 trace 一样直接放入make_subplots网格,因此需要一套特殊的坐标轴重组技巧。读完本文,你将掌握「先创建图工厂图 → 重映射 x/y 坐标轴 → 合并 data 与 layout」的完整工作流,能够自由组合矢场图、流线图与数据表格,构建横排、竖排乃至任意网格布局的多面板图。
为什么图工厂图需要特殊的子图处理
Plotly 的 Python API 中包含一个 figure factory 模块,它提供了一批包装函数,用于创建尚未内置于 plotly.js 中的特殊图表类型。图工厂函数返回的是一个完整的 Figure 对象(plotly.graph_objs.Figure),因此像子图(subplot)这类依赖网格布局的功能,需要以略微不同的方式实现。
从当前仓库的 plotly/figure_factory/init.py 可以看到,plotly.figure_factory模块导出了以下工厂函数:
| 工厂函数 | 图表类型 | 依赖 |
|---|---|---|
create_dendrogram | 树状图 | numpy |
create_quiver | 矢量场(quiver)图 | numpy |
create_streamline | 流线图 | numpy |
create_table | 表格 | numpy(可选 pandas) |
create_trisurf | 三角剖分曲面 | numpy |
create_hexbin_map | 六边形分箱地图 | numpy + pandas |
create_ternary_contour | 三元等高线 | numpy + scikit-image |
其中create_quiver、create_streamline和create_table是子图组合中最常用的三个工厂函数。关键区别在于:普通 trace(如go.Scatter、go.Bar)只是data列表中的一个元素,可以自由指定xaxis/yaxis归属;而图工厂函数内部已经生成了完整的Figure(data, layout),坐标轴名称、布局域(domain)都已确定,想要放入子图就必须手动重组。
核心思路:手动重映射坐标轴后合并 Figure
图工厂子图的基本流程分为三步:
- 分别创建每个图工厂图(
fig1、fig2……); - 改写坐标轴引用:把每个 trace 的
xaxis/yaxis指向新坐标轴名(如x1/y1、x2/y2),并在layout中初始化这些坐标轴、设置各自的domain(归一化画布上的位置区间)和anchor(锚定关系); - 合并:用
go.Figure()新建空图,通过add_traces加入各图的data,再通过layout.update依次合并各图的layout。
示例一:垂直布局的 Quiver 与 Streamline
首先创建两个要放进子图的图工厂图:
import plotly.figure_factory as ff import plotly.graph_objects as go import numpy as np ## Create first figure x1, y1 = np.meshgrid(np.arange(0, 2, .2), np.arange(0, 2, .2)) u1 = np.cos(x1) * y1 v1 = np.sin(x1) * y1 fig1 = ff.create_quiver(x1, y1, u1, v1, name='Quiver') ## Create second figure x = np.linspace(-3, 3, 100) y = np.linspace(-3, 3, 100) Y, X = np.meshgrid(x, y) u = -1 - X**2 + Y v = 1 + X - Y**2 fig2 = ff.create_streamline(x, y, u, v, arrow_scale=.1, name='Streamline')接着编辑两张图的 x/y 轴属性来构成子图。核心操作是:把每个 trace 的xaxis/yaxis改成显式的坐标轴名,然后为该坐标轴设置anchor与domain:
for i in range(len(fig1.data)): fig1.data[i].xaxis = 'x1' fig1.data[i].yaxis = 'y1' fig1.layout.xaxis1.update({'anchor': 'y1'}) fig1.layout.yaxis1.update({'anchor': 'x1', 'domain': [.55, 1]}) for i in range(len(fig2.data)): fig2.data[i].xaxis = 'x2' fig2.data[i].yaxis = 'y2' # initialize xaxis2 and yaxis2 fig2['layout']['xaxis2'] = {} fig2['layout']['yaxis2'] = {} fig2.layout.xaxis2.update({'anchor': 'y2'}) fig2.layout.yaxis2.update({'anchor': 'x2', 'domain': [0, .45]})最后合并 data 与 layout,得到完整图形:
fig = go.Figure() fig.add_traces([fig1.data[0], fig2.data[0]]) fig.layout.update(fig1.layout) fig.layout.update(fig2.layout) fig.show()要点解读:
domain: [0.55, 1]与domain: [0, 0.45]把两张图分别放在画布的上半区与下半区,实现垂直堆叠;anchor用于指定坐标轴彼此锚定:xaxis1锚定y1、yaxis1锚定x1,两个坐标轴互为锚定对象,图表才会被正确放置;- 由于图工厂的
layout中原本只包含默认的xaxis/yaxis,直接访问fig2.layout.xaxis2之前必须先用fig2['layout']['xaxis2'] = {}显式初始化,否则会因属性不存在而报错。
实战扩展一:水平布局的表格 + 折线图
图工厂的create_table非常适合与普通 trace 混排。下面把一张冰球联赛统计表放在左侧,把折线图放在右侧:
import plotly.graph_objects as go import plotly.figure_factory as ff table_data = [['Team', 'Wins', 'Losses', 'Ties'], ['Montréal<br>Canadiens', 18, 4, 0], ['Dallas Stars', 18, 5, 0], ['NY Rangers', 16, 5, 0], ['Boston<br>Bruins', 13, 8, 0], ['Chicago<br>Blackhawks', 13, 8, 0], ['LA Kings', 13, 8, 0], ['Ottawa<br>Senators', 12, 5, 0]] fig = ff.create_table(table_data, height_constant=60) teams = ['Montréal Canadiens', 'Dallas Stars', 'NY Rangers', 'Boston Bruins', 'Chicago Blackhawks', 'LA Kings', 'Ottawa Senators'] GFPG = [3.54, 3.48, 3.0, 3.27, 2.83, 2.45, 3.18] GAPG = [2.17, 2.57, 2.0, 2.91, 2.57, 2.14, 2.77] trace1 = go.Scatter(x=teams, y=GFPG, marker=dict(color='#0099ff'), name='Goals For<br>Per Game', xaxis='x2', yaxis='y2') trace2 = go.Scatter(x=teams, y=GAPG, marker=dict(color='#404040'), name='Goals Against<br>Per Game', xaxis='x2', yaxis='y2') fig.add_traces([trace1, trace2]) # initialize xaxis2 and yaxis2 fig['layout']['xaxis2'] = {} fig['layout']['yaxis2'] = {} # Edit layout for subplots fig.layout.xaxis.update({'domain': [0, .5]}) fig.layout.xaxis2.update({'domain': [0.6, 1.]}) # The graph's yaxis MUST BE anchored to the graph's xaxis fig.layout.yaxis2.update({'anchor': 'x2'}) fig.layout.yaxis2.update({'title': 'Goals'}) # Update the margins to add a title and see graph x-labels. fig.layout.margin.update({'t': 50, 'b': 100}) fig.layout.update({'title': '2016 Hockey Stats'}) fig.show()注意这里 table 是直接由ff.create_table(table_data, height_constant=60)生成的 Figure(其 trace 默认使用x1/y1),所以无需新建空 Figure;随后用fig.add_traces([trace1, trace2])把两个折线 trace 追加进去,并把它们指向新初始化的x2/y2。表格占用xaxis的domain: [0, .5](左半),折线图占用xaxis2的domain: [0.6, 1.](右半)。
实战扩展二:垂直布局的表格 + 柱状图
若想把表格放在上方、图表放在下方,则改为操纵yaxis的domain:
import plotly.graph_objects as go import plotly.figure_factory as ff # Add table data table_data = [['Team', 'Wins', 'Losses', 'Ties'], ['Montréal<br>Canadiens', 18, 4, 0], ['Dallas Stars', 18, 5, 0], ['NY Rangers', 16, 5, 0], ['Boston<br>Bruins', 13, 8, 0], ['Chicago<br>Blackhawks', 13, 8, 0], ['Ottawa<br>Senators', 12, 5, 0]] # Initialize a figure with ff.create_table(table_data) fig = ff.create_table(table_data, height_constant=60) # Add graph data teams = ['Montréal Canadiens', 'Dallas Stars', 'NY Rangers', 'Boston Bruins', 'Chicago Blackhawks', 'Ottawa Senators'] GFPG = [3.54, 3.48, 3.0, 3.27, 2.83, 3.18] GAPG = [2.17, 2.57, 2.0, 2.91, 2.57, 2.77] # Make traces for graph trace1 = go.Bar(x=teams, y=GFPG, xaxis='x2', yaxis='y2', marker=dict(color='#0099ff'), name='Goals For<br>Per Game') trace2 = go.Bar(x=teams, y=GAPG, xaxis='x2', yaxis='y2', marker=dict(color='#404040'), name='Goals Against<br>Per Game') # Add trace data to figure fig.add_traces([trace1, trace2]) # initialize xaxis2 and yaxis2 fig['layout']['xaxis2'] = {} fig['layout']['yaxis2'] = {} # Edit layout for subplots fig.layout.yaxis.update({'domain': [0, .45]}) fig.layout.yaxis2.update({'domain': [.6, 1]}) # The graph's yaxis2 MUST BE anchored to the graph's xaxis2 and vice versa fig.layout.yaxis2.update({'anchor': 'x2'}) fig.layout.xaxis2.update({'anchor': 'y2'}) fig.layout.yaxis2.update({'title': 'Goals'}) # Update the margins to add a title and see graph x-labels. fig.layout.margin.update({'t': 75, 'l': 50}) fig.layout.update({'title': '2016 Hockey Stats'}) # Update the height because adding a graph vertically will interact with # the plot height calculated for the table fig.layout.update({'height': 800}) # Plot! fig.show()与水平版相比,垂直版的关键差异:
- 通过
yaxis.update({'domain': [0, .45]})与yaxis2.update({'domain': [.6, 1]})划分上下区域; - 图表坐标轴需要双向锚定:
yaxis2锚定x2,xaxis2锚定y2; - 因为
create_table会根据行数自动计算画布高度(见下文源码),竖向叠加图表后会挤压空间,因此需要手动fig.layout.update({'height': 800})撑高画布; - 边距
margin需要预留顶部(t: 75)与左侧(l: 50)空间,以免标题与 y 轴标签被截断。
方案对比:为什么不直接用 make_subplots
plotly.subplots.make_subplots是 Plotly 创建子图网格的标准入口(详见 plotly/subplots.py 中的make_subplots(rows, cols, shared_xaxes, shared_yaxes, start_cell, horizontal_spacing, vertical_spacing, subplot_titles, column_widths, row_heights, specs, insets, ...)),它预先生成xaxis1/yaxis1、xaxis2/yaxis2等一套完整的坐标轴与 domain。对于普通 trace(go.Scatter、go.Bar等),直接add_trace(trace, row, col)即可自动归属坐标轴。
但图工厂返回的是完整 Figure 而非单个 trace,且其内部 layout 自带一套坐标轴,直接塞进make_subplots网格会与网格预置的坐标轴冲突。因此官方示例采用「手动重映射坐标轴」这一更底层的方案,这同时也是理解 Plotly 坐标轴模型(domain+anchor+xaxis/yaxis引用)的最佳切入点。
如果希望使用更现代的写法,也可以在make_subplots建好的网格中手动为 trace 指定坐标轴引用:例如先用make_subplots(rows=1, cols=2)得到预置的xaxis1/yaxis1、xaxis2/yaxis2,再分别把表格 heatmap trace 与普通 scatter trace 的xaxis/yaxis指向对应轴名后add_trace。两种思路本质相同——坐标轴名称与 domain 的对应关系最终决定了每个 trace 落在哪个面板。
图工厂函数参数深度解析
create_table的参数
create_table定义于 plotly/figure_factory/_table.py,签名如下:
create_table(table_text, colorscale=None, font_colors=None, index=False, index_title="", annotation_offset=0.45, height_constant=30, hoverinfo="none", **kwargs)| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
table_text | pandas.DataFrame或list[list] | 必填 | 表格数据;传入 DataFrame 时自动取columns作为表头 |
colorscale | str或list[list] | [[0, '#00083e'], [0.5, '#ededee'], [1, '#ffffff']] | 表格配色:0 对应表头色,0.5 与 1 对应两条交替行色(设成相同值可取消斑马纹) |
font_colors | list | ['#ffffff', '#000000', '#000000'] | 字体颜色,长度须为 1、3 或len(table_text);为 3 时依次对应表头、奇数行、偶数行 |
index | bool | False | 是否生成表头色的索引列 |
index_title | str | '' | 索引列标题 |
annotation_offset | float | 0.45 | 单元格文本相对格心的偏移量 |
height_constant | int | 30 | 行高系数,画布高度 =行数 × height_constant + 50 |
hoverinfo | str | 'none' | hover 信息内容 |
**kwargs | - | - | 透传给底层 heatmap trace 的属性 |
源码实现的几个关键点:
- 表格内部由一个heatmap trace承载底色(
z为行索引矩阵:表头行取 0、奇数行取 0.5、偶数行取 1,配合 colorscale 形成斑马纹),所有单元格文字通过annotations逐格绘制(见_Table.make_table_annotations),表头与索引列文字自动加粗(<b>...</b>); font_colors的合法性由validate_table校验,长度只能是 1、3 或行数,否则抛出PlotlyError;- 布局中
yaxis设置了autorange="reversed",保证表头位于顶部。
create_quiver的参数
create_quiver定义于 plotly/figure_factory/_quiver.py:
create_quiver(x, y, u, v, scale=0.1, arrow_scale=0.3, angle=math.pi / 9, scaleratio=None, **kwargs)| 参数 | 默认值 | 说明 |
|---|---|---|
x,y | 必填 | 箭头起点坐标(list 或 ndarray) |
u,v | 必填 | 箭头的 x/y 方向分量 |
scale | 0.1 | 箭头整体缩放系数([0, 1],越小越不容易重叠) |
arrow_scale | 0.3 | 箭杆长度乘以该系数得到箭翼长度 |
angle | pi/9 | 箭翼张开角(弧度) |
scaleratio | None | y 轴与 x 轴比例(scale_y/scale_x);传入后布局会设置yaxis.scaleratio并锚定scaleanchor="x",用于保持箭头几何比例 |
**kwargs | - | 透传给go.Scatter(如name、line) |
quiver 图内部由两个 scatter 组合:箭杆(barbs)与箭翼(arrowheads),通过None分隔成多段线段,最终mode="lines"。文档示例中的fig1 = ff.create_quiver(x1, y1, u1, v1, name='Quiver')即使用默认scale=0.1、arrow_scale=0.3。
create_streamline的参数
create_streamline定义于 plotly/figure_factory/_streamline.py:
create_streamline(x, y, u, v, density=1, angle=math.pi / 9, arrow_scale=0.09, **kwargs)| 参数 | 默认值 | 说明 |
|---|---|---|
x,y | 必填 | 一维且等间距的坐标数组 |
u,v | 必填 | 二维速度场分量(ndarray) |
density | 1 | 流线密度,内部乘以 30 得到积分种子网格(与 matplotlib 等工具量纲对齐) |
angle | pi/9 | 箭头张开角(弧度) |
arrow_scale | 0.09 | 箭翼长度缩放系数 |
**kwargs | - | 透传给go.Scatter(如name) |
实现层面,validate_streamline会校验x、y是否等间距(相邻步长差超过 0.0001 即抛PlotlyError),并要求 numpy 可用;流线轨迹采用RK4 四阶龙格-库塔积分(rk4_integrate,算法源自 Bokeh 的 streamline 实现)沿速度场正反向追踪,再用网格空白标记避免轨迹重叠。文档示例中fig2 = ff.create_streamline(x, y, u, v, arrow_scale=.1, name='Streamline')将默认箭头比例略调大以增强可视化效果。
测试与验证
仓库在 tests/test_optional/test_figure_factory/test_figure_factory.py 中提供了工厂函数的行为验证,例如:
create_streamline对非等间距x/y输入抛出ValueError/PlotlyError的校验用例;create_quiver在传入scale=1, scaleratio=0.5时生成固定比例箭头的用例。
这些测试印证了参数校验逻辑与「返回完整plotly.graph_objs.Figure」的契约,也说明在组合子图前,工厂函数产出的 data/layout 结构是稳定可预期的。
小结与进一步阅读
图工厂子图的本质是一套「坐标轴重映射」流程:把工厂函数产出的 trace 显式绑定到新坐标轴,通过domain划分画布区域、anchor建立轴间锚定,最后合并 layout。掌握了这套机制,无论表格 + 图表、quiver + streamline,还是任意工厂图与普通 trace 的组合,都能按需排布。
进一步参考:
- 图工厂模块总览与各工厂函数清单:doc/python/figure-factories.md
- 表格工厂专项文档:doc/python/figure-factory-table.md
- Quiver 图专项文档:doc/python/quiver-plots.md
- Streamline 图专项文档:doc/python/streamline-plots.md
- 多坐标轴与子图布局专题:doc/python/multiple-axes.md 与 doc/python/mixed-subplots.md
make_subplots完整参数说明见 plotly/subplots.py- 坐标轴、domain、anchor 等图表属性的 API 参考见 doc/apidoc/plotly.graph_objects.rst
- 数据可视化
- 数据分析
【免费下载链接】plotly.py
The interactive graphing library for Python :sparkles:
相关推荐
MiniChat-3B vs 主流对话模型:为什么这个3B参数模型值得关注?
MiniChat 3B vs 主流对话模型:为什么这个3B参数模型值得关注? 在大语言模型层出不穷的今天,MiniChat 3B以其仅30亿参数的轻量级身材,在
数据可视化数据分析Plotly Python 混合子图(Mixed Subplots)实战:用 make_subplots 在同一画布组合 scattergeo、bar 与 surface 等异构图表
Plotly Python 混合子图(Mixed Subplots)实战:用 make_subplots 在同一画布组合 scattergeo、bar 与 su
数据可视化数据分析Plotly.py Figure Factory 高阶图表工厂指南:7 大复杂图表一键生成
Plotly.py Figure Factory 高阶图表工厂指南:7 大复杂图表一键生成 plotly.figure_factory 是 Plotly Pyt
数据可视化数据分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考