目录17

文章Matplotlib第 34 章

表格与偏移框

Matplotlib · 第 34 / 43 章

表格 table()

Axes.table() 在坐标轴里塞一张真表格(由一堆 Cell 拼成,每个 Cell 是一个带文字的矩形)。它默认铺满整幅、loc='bottom',所以实际用起来几乎一定要配 bbox 或先 ax.axis('off')。

Python16 行
Axes.table(
    cellText=None,        # 二维列表:行 × 列
    cellColours=None,     # 与 cellText 同形的颜色二维列表
    cellLoc="right",      # 数据格的对齐;★ 默认右对齐
    colWidths=None,       # 每列宽度,单位是**轴宽的倍数**,总和建议 ≈1
    rowLabels=None,       # 行头(放最左一列)
    rowColours=None,      # 行头底色列表
    rowLoc="left",        # 行头对齐
    colLabels=None,       # 列头(放最上一行)
    colColours=None,      # 列头底色列表
    colLoc="center",      # 列头对齐
    loc="bottom",         # 整表相对 axes 的位置
    bbox=None,            # 4 元组或 Bbox,给定位;给了 loc 就失效
    edges="closed",       # 'closed' / 'open' / 'horizontal' / 'vertical'
    **kwargs
)
参数 一句话 默认值的影响
cellText 表格正文 必填(或用 cellColours 造空表)
cellLoc / colLoc / rowLoc 三处对齐 right / center / left
colWidths 列宽(轴宽倍数) None → 均分,表铺满整个 axes
colLabels / rowLabels 表头 不给就没有表头行/列
colColours / rowColours 表头变色 None 全白
loc / bbox 放哪儿 'bottom' 铺在下沿;给 bbox 覆盖它
edges 画哪几条边 'closed' 四边全画

最小示例

Python12 行
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(5, 2))
ax.axis('off')
tbl = ax.table(cellText=[['2025', '182', '+8.2%'],
                         ['2026', '205', '+12.6%']],
               colLabels=['年份', '销售额', '同比'],
               colWidths=[.3, .3, .4], loc='center', cellLoc='center')
tbl.auto_set_font_size(False)
tbl.set_fontsize(10)
tbl.scale(1, 1.8)
plt.show()

table 的默认样子与美化版对比

colWidths 与 scale:调大小只有这两条路

Python2 行
tbl = ax.table(cellText=[['a', 'b']], colWidths=[.4, .6])   # 列宽按轴宽比例
tbl.scale(1.0, 2.0)      # 横向 1 倍、纵向 2 倍

colWidths 的数值是轴宽的倍数,全列加起来建议接近 1(超过会伸出轴外)。scale(sx, sy) 才是整体缩放——注意它缩放的是格子的相对尺寸,不改变表的锚点,所以放大后表会从原锚点往外长。

行高没有 rowHeights 参数,只能靠 scale 的第二参数 sy,或者逐个 cell.set_height()。

get_celld():索引规则会随表头变化

这是最容易写错的地方。get_celld() 返回 {(row, col): Cell},而行号是否从 0 开始取决于给没给 colLabels:

Python14 行
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
t1 = ax.table(cellText=[['a', 'b'], ['c', 'd']])
print(sorted(t1.get_celld()))
# 输出: [(0, 0), (0, 1), (1, 0), (1, 1)]

t2 = ax.table(cellText=[['a', 'b'], ['c', 'd']], colLabels=['X', 'Y'],
              rowLabels=['r1', 'r2'])
print(sorted(t2.get_celld()))
# 输出: [(0, 0), (0, 1), (1, -1), (1, 0), (1, 1), (2, -1), (2, 0), (2, 1)]

print(repr(t2.get_celld()[(0, 0)].get_text().get_text()))
# 输出: 'X'

规则:

给了什么 (0, 0) 是 数据从哪格起 行头在哪
只有 cellText 第一格数据 (0, 0) —
+ colLabels 列表头第一格 (1, 0) —
+ colLabels 和 rowLabels 列表头第一格 (1, 0) col = -1 那一列

所以「给第一行数据上色」这种需求,代码必须先判断有没有表头:

Python6 行
tbl = ax.table(cellText=[['a', 'b'], ['c', 'd']], colLabels=['X', 'Y'])
for (r, c), cell in tbl.get_celld().items():
    if r == 0:
        cell.set_facecolor('#f1f3f5')      # 有 colLabels 时 r=0 是表头
    elif r % 2 == 1:
        cell.set_facecolor('#f8f9fa')      # 斑马纹

逐格改样式:get_celld() 是唯一入口

Table 本身只有 scale / set_fontsize / auto_set_font_size / auto_set_column_width 四个整体方法,所有细粒度样式(底色、边框、加粗、文字颜色)都得逐格改:

Python15 行
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(5, 2))
ax.axis('off')
tbl = ax.table(cellText=[['2025', '182'], ['2026', '205']],
               colLabels=['年份', '销售额'], loc='center')

for (r, c), cell in tbl.get_celld().items():
    cell.set_linewidth(.6)
    if r == 0:
        cell.set_facecolor('#f1f3f5')
        cell.set_text_props(weight='bold', color='#343a40')
    elif c == 1:
        cell.set_text_props(ha='right')      # 数值列右对齐
plt.show()

Cell 继承自 Rectangle,所以 set_facecolor / set_edgecolor / set_linewidth / set_height 全可用;文字是 cell.get_text(),或直接 cell.set_text_props(**kwargs)。

edges:只画需要的边

Python10 行
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
for i, e in enumerate(['closed', 'open', 'horizontal', 'vertical']):
    t = ax.table(cellText=[['a']], edges=e)
    print(e, repr(t.get_celld()[(0, 0)].visible_edges))
# 输出: closed 'BRTL'
# 输出: open ''
# 输出: horizontal 'BT'
# 输出: vertical 'RL'

visible_edges 是「上下左右各画不画」的字符串,取值来自 BRTL(Bottom / Right / Top / Left)。三线表就靠这个做:整体用 edges='open',再逐格给表头上边、表头下边加线。

返回对象
Python9 行
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
tbl = ax.table(cellText=[['a', 'b'], ['c', 'd']])
print(type(tbl).__name__)
# 输出: Table

print(type(tbl.get_celld()[(0, 0)]).__name__)
# 输出: Cell

Table 是 Artist 的子类,get_celld() 返回 {(row, col): Cell} 的字典。整体可调的只有这几个:

Python7 行
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
tbl = ax.table(cellText=[['2025', '182'], ['2026', '205']], colLabels=['年份', '销售额'])
print([n for n in dir(tbl) if n in ('scale', 'set_fontsize', 'auto_set_font_size',
                                    'auto_set_column_width', 'get_celld')])
# 输出: ['auto_set_column_width', 'auto_set_font_size', 'get_celld', 'scale', 'set_fontsize']

auto_set_font_size(True)(默认)会让字号随格子大小自适应——这就是为什么缩放后字号会莫名变大变小。想要固定字号必须先 auto_set_font_size(False),再 set_fontsize()。

注意事项

  • loc='bottom' 是默认值,表会铺在轴下沿并盖住坐标轴;不用 bbox 就基本一定要先 ax.axis('off')。
  • get_celld() 的行号会因 colLabels 存在而整体下移 1,(0, 0) 在有无表头时指的不是同一格。
  • rowLabels 存在时行头在 col = -1,不是 col = 0——遍历时别把行头当数据。
  • 没有 rowHeights 参数;改行高只能 scale(1, sy) 或逐格 set_height。
  • auto_set_font_size 默认开启,会覆盖你设的 fontsize;要固定字号先关掉它。
  • colWidths 是轴宽倍数,加起来超过 1 会伸出轴外,且不会自动换行。
  • Table 不参与 tight_layout / constrained_layout,位置全靠 bbox 或 loc 手动调。

综合示例:三线表 / 斑马纹表 / 带行头的表

Python69 行
import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 3, figsize=(13, 3))

# ① 三线表:只留顶线、表头下线和底线
ax = axes[0]
ax.axis('off')
tbl = ax.table(cellText=[['2024', '165', '—'], ['2025', '182', '+10.3%'],
                         ['2026', '205', '+12.6%']],
               colLabels=['年份', '销售额', '同比'],
               colWidths=[.3, .34, .36], loc='center', cellLoc='center',
               edges='open')
for (r, c), cell in tbl.get_celld().items():
    cell.set_linewidth(0)
tbl.auto_set_font_size(False)
tbl.set_fontsize(9)
tbl.scale(1, 1.9)
nrow = len(tbl.get_celld()) // 3
for (r, c), cell in tbl.get_celld().items():
    if r == 0:
        cell.visible_edges = 'BT'
        cell.set_linewidth(1.2)
    if r == nrow - 1:
        cell.visible_edges = 'B'
        cell.set_linewidth(1.2)
ax.set_title('三线表(edges + visible_edges)', fontsize=9)

# ② 斑马纹 + 数值右对齐
ax = axes[1]
ax.axis('off')
tbl = ax.table(cellText=[['华东', '182', '+8.2%'], ['华南', '145', '+3.1%'],
                         ['华北', '203', '+11.4%']],
               colLabels=['区域', '销售额', '同比'],
               colWidths=[.28, .34, .38], loc='center')
tbl.auto_set_font_size(False)
tbl.set_fontsize(9)
tbl.scale(1, 1.9)
for (r, c), cell in tbl.get_celld().items():
    cell.set_linewidth(.5)
    if r == 0:
        cell.set_facecolor('#343a40')
        cell.set_text_props(color='white', weight='bold')
    else:
        cell.set_facecolor('#ffffff' if r % 2 else '#f1f3f5')
        if c > 0:
            cell.set_text_props(ha='right')
ax.set_title('斑马纹 + 数值右对齐', fontsize=9)

# ③ 带行头(col = -1)
ax = axes[2]
ax.axis('off')
tbl = ax.table(cellText=[['182', '+8.2%'], ['145', '+3.1%'], ['203', '+11.4%']],
               rowLabels=['华东', '华南', '华北'],
               colLabels=['销售额', '同比'],
               colWidths=[.5, .5], loc='center')
tbl.auto_set_font_size(False)
tbl.set_fontsize(9)
tbl.scale(1, 1.9)
for (r, c), cell in tbl.get_celld().items():
    cell.set_linewidth(.5)
    if c == -1:
        cell.set_facecolor('#f1f3f5')     # 行头
        cell.set_text_props(weight='bold')
    elif r == 0:
        cell.set_facecolor('#f1f3f5')
ax.set_title('带行头(col = -1)', fontsize=9)

plt.tight_layout()
plt.show()

第 ③ 格的关键是 c == -1 那一列才是行头,(0, 0) 依然是列表头第一格——两个偏移方向不同,只能分别判断。

偏移框 AnnotationBbox

annotate() 只能放一段文字。要放「多段文字拼接」「小图标」「任意 artist」,就用 AnnotationBbox——它把一个盒子钉在数据点旁,偏移量用点计算,不随数据缩放变。

Python16 行
AnnotationBbox(
    offsetbox,                # ★ 被钉的盒子:TextArea / HPacker / VPacker / DrawingArea / OffsetImage
    xy,                       # ★ 钉在哪个点
    xybox=None,               # 盒子自己放在哪;不给就等于 xy
    xycoords="data",          # xy 的坐标系
    boxcoords=None,           # xybox 的坐标系;★ None → 跟随 xycoords
    *,
    frameon=True,             # 画不画外框
    pad=0.4,                  # 框内边距(字号倍数)
    annotation_clip=None,      # 锚点出界时是否隐藏
    box_alignment=(0.5, 0.5),  # 盒子的哪个点对齐到 xybox
    bboxprops=None,           # 外框样式,写法同 text 的 bbox
    arrowprops=None,          # 箭头,写法同 annotate
    fontsize=None,
    **kwargs
)
参数 一句话 默认值的影响
offsetbox 盒子里装啥 必填;TextArea 只吃 textprops
xy / xybox 钉的点 / 盒子放的点 xybox=None → 盒子压在点上
xycoords / boxcoords 两套坐标系 boxcoords=None → 跟随 xycoords('data')
box_alignment 盒子哪个点对齐 xybox (0.5, 0.5) 居中
frameon / pad / bboxprops 外框 默认有框、pad=0.4
arrowprops 从盒子指向 xy 的箭头 None

最小示例

Python14 行
import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnnotationBbox, TextArea

fig, ax = plt.subplots()
x = [0, 1, 2, 3, 4]
y = [1, 3, 2, 5, 4]
ax.plot(x, y, 'o-', color='#4C72B0')

for xi, yi, lab in zip(x, y, ['甲', '乙', '丙', '丁', '戊']):
    ab = AnnotationBbox(TextArea(lab, textprops=dict(color='#1c7ed6', size=9)),
                        (xi, yi), xybox=(10, 10), xycoords='data',
                        boxcoords='offset points', frameon=False, pad=0)
    ax.add_artist(ab)          # ★ 必须手动挂上去
plt.show()

AnnotationBbox 的四组关键参数

offsetbox:盒子里能装什么

类 干什么 关键参数
TextArea 一段文字 textprops=dict(...)
HPacker 横向拼多个盒子 children / sep / pad / align
VPacker 纵向拼多个盒子 同上
DrawingArea 一块可画图的画布 width / height / clip
OffsetImage 一张图片 arr / zoom / cmap

TextArea 没有 color= 这类关键字,样式必须塞进 textprops:

Python9 行
from matplotlib.offsetbox import TextArea

try:
    TextArea('x', color='red')
except TypeError as e:
    print(e)
# 输出: TextArea.__init__() got an unexpected keyword argument 'color'

t = TextArea('x', textprops=dict(color='red', size=10, weight='bold'))   # ✓

HPacker 用来做「年份 + 数值」这种混排,两段可以各设各的颜色字重:

Python6 行
from matplotlib.offsetbox import HPacker, TextArea

box = HPacker(children=[TextArea('2026 ', textprops=dict(size=9)),
                        TextArea('+12.6%', textprops=dict(size=9, weight='bold',
                                                          color='#C44E52'))],
              align='center', pad=0, sep=2)

xybox + boxcoords:不写 boxcoords 就白折腾

这是最容易踩的一处。boxcoords 默认 None,意思是跟随 xycoords,而 xycoords 默认 'data':

Python7 行
import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnnotationBbox, TextArea

fig, ax = plt.subplots()
ab = AnnotationBbox(TextArea('甲'), (1, 2))
print(ab.xycoords, ab.boxcoords, ab.xybox)
# 输出: data data (1, 2)

所以 xybox=(10, 10) 会被当成「数据坐标 (10, 10)」——图上一共只有 0~4,盒子直接飞出画布。想要「相对数据点偏移 10 点」必须显式写:

Python8 行
import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnnotationBbox, TextArea

fig, ax = plt.subplots()
ab = AnnotationBbox(TextArea('甲'), (1, 2),
                    xybox=(10, 10), boxcoords='offset points')
ax.add_artist(ab)
plt.show()

'offset points' 是这里唯一实用的选择,另外还能用 'offset pixels'(受 DPI 影响,跨屏不稳定)。

box_alignment:盒子内部哪个角贴 xybox

Python9 行
import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnnotationBbox, TextArea

fig, ax = plt.subplots()
for al, off in [((0, 0), (-52, 26)), ((1, 1), (26, -26))]:
    ab = AnnotationBbox(TextArea('x'), (2, 3), xybox=off, xycoords='data',
                        boxcoords='offset points', box_alignment=al)
    ax.add_artist(ab)
plt.show()

(0, 0) = 盒子左下角落在 xybox、(1, 1) = 右上角、(0.5, 0.5)(默认)= 中心。它改的是盒子内部的对齐点,不是偏移量——偏移只由 xybox 决定。

frameon / pad / bboxprops:外框三层

Python13 行
import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnnotationBbox, TextArea

fig, ax = plt.subplots()
ax.add_artist(AnnotationBbox(
    TextArea('无框'), (2, 3), xybox=(0, 20),
    boxcoords='offset points', frameon=False))

ax.add_artist(AnnotationBbox(
    TextArea('有框'), (2, 3), xybox=(0, -20),
    boxcoords='offset points', frameon=True, pad=0.6,
    bboxprops=dict(boxstyle='round', fc='#fff3bf', ec='#f08c00')))
plt.show()

frameon=False 等价于不画 patch;pad 是框内边距(字号倍数);bboxprops 的写法和 text 的 bbox 完全一致,boxstyle / fc / ec / lw 都能用。

arrowprops:从盒子指向锚点

Python10 行
import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnnotationBbox, TextArea

fig, ax = plt.subplots()
ab = AnnotationBbox(TextArea('2026 +12.6%'), (3, 5), xybox=(0, -34),
                    boxcoords='offset points', frameon=True, pad=.4,
                    bboxprops=dict(boxstyle='round', fc='#fff3bf', ec='#f08c00'),
                    arrowprops=dict(arrowstyle='->', color='#f08c00'))
ax.add_artist(ab)
plt.show()

arrowprops 的键和 annotate 里那张表一模一样(arrowstyle / connectionstyle / shrinkA / shrinkB / color / lw)。方向也一致:箭头从「盒子所在位置(xybox)」指向「锚点(xy)」,也就是箭头尖永远落在数据点上,盒子在哪儿它就从哪儿出发。

返回对象
Python15 行
import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnnotationBbox, TextArea

fig, ax = plt.subplots()
ab = AnnotationBbox(TextArea('甲'), (1, 2), xybox=(10, 10),
                    boxcoords='offset points')
print(type(ab).__name__)
# 输出: AnnotationBbox

print(ab in ax.get_children())
# 输出: False

ax.add_artist(ab)
print(ab in ax.get_children())
# 输出: True

AnnotationBbox 构造完不会自动上图——annotate() 会自己挂,AnnotationBbox 不会,必须 ax.add_artist(ab)。这是「我明明创建了却什么都看不到」的第一号原因。

它是 Artist 子类,所以 remove() / set_visible() / set_zorder() 都能用:

Python9 行
import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnnotationBbox, TextArea

fig, ax = plt.subplots()
ab = AnnotationBbox(TextArea('甲'), (1, 2))
ax.add_artist(ab)
ab.set_zorder(10)
print(ab.get_zorder())
# 输出: 10

内部读法:ab.offsetbox(那个盒子)、ab.patch(外框 FancyBboxPatch)、ab.xybox(落点)。想改文字得往下挖到 ab.offsetbox 里的 TextArea,不能直接 ab.set_text。

注意事项

  • 必须 ax.add_artist(ab),构造完不会自动进图。实测 ab in ax.get_children() 构造后是 False。
  • TextArea 没有 color= / fontsize=,样式只能走 textprops=dict(...),传错直接 TypeError。
  • boxcoords 不写就跟随 xycoords('data'),xybox=(10, 10) 会变成数据坐标偏移、飞出画布。
  • box_alignment 改的是盒子内部对齐点,不是偏移量;偏移只由 xybox 决定。
  • pad 是字号倍数,pad=0 会让框贴住文字墨迹。
  • bboxprops 和 text 的 bbox 写法完全一致,但必须传字典,传字符串同样报 AttributeError。
  • 图例、tight_layout 都不认 AnnotationBbox 的尺寸,可能压到别的元素上;固定位置建议用 transform=ax.transAxes + xybox。
  • 和 annotate 的分工:一段文字 + 箭头用 annotate;多段拼接、图片、自定义画布用 AnnotationBbox。

综合示例:数据点标签 / 多段混排 / 图片标注

Python48 行
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.offsetbox import AnnotationBbox, TextArea, HPacker, VPacker

fig, axes = plt.subplots(1, 3, figsize=(13, 3.8))
x = np.arange(5)

# ① 每个点旁挂一个偏移标签
ax = axes[0]
y = np.array([1, 3, 2, 5, 4])
ax.plot(x, y, 'o-', color='#4C72B0')
for xi, yi, lab in zip(x, y, list('甲乙丙丁戊')):
    ax.add_artist(AnnotationBbox(
        TextArea(lab, textprops=dict(color='#1c7ed6', size=9)),
        (xi, yi), xybox=(0, 12), xycoords='data',
        boxcoords='offset points', frameon=False, pad=0))
ax.set_title('offset points 标签', fontsize=9)

# ② 两段文字混排(不同颜色/字重)
ax = axes[1]
ax.bar(['A', 'B'], [18, 24], color='#4C72B0')
for i, (v, d) in enumerate(zip([18, 24], ['+8.2%', '+12.6%'])):
    box = HPacker(children=[TextArea(f'{v} ', textprops=dict(size=9)),
                            TextArea(d, textprops=dict(size=9, weight='bold',
                                                       color='#C44E52'))],
                  align='center', pad=0, sep=1)
    ax.add_artist(AnnotationBbox(
        box, (i, v), xybox=(0, 14), xycoords='data',
        boxcoords='offset points', frameon=True, pad=.35,
        bboxprops=dict(boxstyle='round', fc='white', ec='0.75')))
ax.set_ylim(0, 30)
ax.set_title('HPacker 混排', fontsize=9)

# ③ 两行文字竖排 + 箭头指向数据点
ax = axes[2]
ax.plot(x, x ** 1.6, 'o-', color='#4C72B0')
box = VPacker(children=[TextArea('峰值区', textprops=dict(size=9, weight='bold')),
                        TextArea('x=4, y≈8.6', textprops=dict(size=8, color='0.4'))],
              align='center', pad=0, sep=1)
ax.add_artist(AnnotationBbox(
    box, (4, 4 ** 1.6), xybox=(-30, 26), xycoords='data',
    boxcoords='offset points', frameon=True, pad=.4,
    bboxprops=dict(boxstyle='round,pad=0.4', fc='#fff3bf', ec='#f08c00'),
    arrowprops=dict(arrowstyle='->', color='#f08c00', shrinkB=4)))
ax.set_title('VPacker + 箭头', fontsize=9)

plt.tight_layout()
plt.show()

第 ③ 格把 VPacker(竖排两块文字)和 arrowprops 一起用了:盒子在左上方,箭头从盒子底边指向数据点。shrinkB=4 让箭头尖和数据点之间留 4pt,避免压住 marker。