目录21

文章Matplotlib第 28 章

范围、方向与显隐

Matplotlib · 第 28 / 43 章

范围:set_xlim / margins / autoscale

坐标轴的范围(view limits)和数据范围(data limits)是两回事。plot() 只更新数据范围,视图范围由 autoscale 根据数据范围加边距算出来;一旦你手动设过范围,autoscale 就关掉了。

Python7 行
Axes.set_xlim(left=None, right=None, *, emit=True, auto=False, xmin=None, xmax=None)
Axes.set_ylim(bottom=None, top=None, *, emit=True, auto=False, ymin=None, ymax=None)
Axes.set_xbound(lower=None, upper=None)     # 只给 autoscale 划边界,不关 autoscale
Axes.margins(*margins, x=None, y=None, tight=True)   # 数据两侧留白比例
Axes.autoscale(enable=True, axis="both", tight=None)  # 重新打开 autoscale
Axes.autoscale_view()                        # 立刻按当前数据重算一次
Axes.set_box_aspect(aspect=None)             # 框的宽高比(不是数据比例)
参数 一句话 默认值的影响
left / right 只给一个 只改一端 另一端保持当前值
set_xlim 直接定视图 同时关掉 x 轴 autoscale
set_xbound 给 autoscale 划上下界 autoscale 仍是开的
margins 数据两侧留白比例 0.05,即两端各留 5%
set_box_aspect 框高/框宽 None 由布局决定

最小示例

Python8 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(6))
print(np.round(ax.get_xlim(), 2))
# 输出: [-0.25  5.25]
plt.show()

默认 margins / margins(x=0) / set_xlim / set_box_aspect

数据是 0~5,边界变成 -0.25 ~ 5.25——两端各多出 0.25,正是默认 margins(0.05) 按数据跨度 5 的 5% 算出来的。

上面为什么写 np.round(ax.get_xlim(), 2) 而不是直接 print(ax.get_xlim())?因为 get_xlim() 返回的是 numpy 标量,numpy 2.0 起它的 repr 变了:

Python8 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(6))
print(ax.get_xlim())
# 输出: (np.float64(-0.25), np.float64(5.25))     ← numpy 2.x
# 输出: (-0.25, 5.25)                            ← numpy 1.x,同一个值

数字本身没错,只是显示形式随 numpy 版本变。本文涉及范围、刻度的打印统一用 np.round,输出才能跨版本一致。

注意别用内置 round() 包一层——round() 不改变 numpy 标量类型:

Python6 行
import numpy as np

print(tuple(round(v, 2) for v in np.array([-0.25, 5.25])))
# 输出: (np.float64(-0.25), np.float64(5.25))     ← 还是 np.float64
print(tuple(np.round(np.array([-0.25, 5.25]), 2)))
# 输出: (np.float64(-0.25), np.float64(5.25))     ← 一样没救

要么用 np.round 输出数组,要么 float(v) 显式转成 Python 浮点。

set_xlim 一个参数也行

Python8 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(6))
ax.set_xlim(left=0)
print(np.round(ax.get_xlim(), 2))
# 输出: [0.   5.25]

只给 left 时右端保持不动。反过来 ax.set_xlim(right=10) 只拉右端。这两个关键字是 3.x 才有的写法,老代码用 ax.set_xlim(xmin=0) 等价。

set_xlim 会关掉 autoscale,set_xbound 不会

这是两者唯一的实际差别,也是最容易搞混的地方:

Python14 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot([0, 1, 2])
ax.set_xlim(0, 10)
print(ax.get_autoscalex_on())
# 输出: False          ← set_xlim 之后 autoscale 关了

fig2, ax2 = plt.subplots()
ax2.plot([0, 1, 2])
ax2.set_xbound(0, 10)
print(ax2.get_autoscalex_on())
# 输出: True           ← set_xbound 之后 autoscale 还开着

所以「后面还要继续加数据、但希望范围别乱跳」用 set_xbound;「就要这个范围,之后不许动」用 set_xlim。

想恢复自动缩放,显式打开:

Python11 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot([0, 1, 2])
ax.set_xlim(0, 10)
print(ax.get_autoscalex_on())
# 输出: False
ax.autoscale(enable=True, axis='x')
print(ax.get_autoscalex_on())
# 输出: True

margins:留白比例

Python12 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(6))
ax.margins(x=.5)
print(np.round(ax.get_xlim(), 2))
# 输出: [-2.5  7.5]     ← 数据 0~5,两端各留 2.5(= 5 × 0.5)

ax.margins(x=0)
print(np.round(ax.get_xlim(), 2))
# 输出: [0. 5.]      ← 数据顶到边框

margins 接受两种写法:位置参数 margins(0.1) 同时管 x/y,关键字 margins(x=0, y=.2) 分开管。设 x=0 是画柱状图/直方图的标准动作——柱子本来就该顶到边框。

注意事项

  • 手动 set_xlim / set_ylim 会关闭该轴的 autoscale,之后加数据视图不跟着变。
  • set_xbound 只限制 autoscale 的边界,get_autoscalex_on() 仍是 True。
  • margins(x=0) 是柱状图的标准配置;折线图留 5% 默认边距即可。
  • axis('equal') 改的是数据单位比例(1 个 x 单位 = 1 个 y 单位),set_box_aspect 改的是框的物理宽高比,两者不同。
  • 用 set_box_aspect 后 imshow 的 aspect='equal' 会被覆盖,别同时给。

方向与比例:翻转 / 等比例 / 宽高比

三个容易被当成一回事的东西:翻转向(哪个方向变大)、等比例(1 单位 x 等不等于 1 单位 y)、宽高比(这个矩形框本身长什么样)。

Python6 行
Axes.invert_xaxis()          # 翻转 X(已是反向则变正)
Axes.invert_yaxis()          # 翻转 Y
Axes.set_xlim(3, 0)          # 等价写法:直接给反序范围
Axes.set_aspect(aspect, adjustable=None, anchor=None)  # 数据比例:'equal' / 'auto' / 数值
Axes.axis("equal")           # = set_aspect('equal', adjustable='datalim')
Axes.set_box_aspect(aspect)  # 框物理高/宽比

最小示例

Python9 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(5), np.arange(5) ** 2, 'o-')
ax.invert_yaxis()                # y 轴倒过来(图像坐标常用)
print(np.round(ax.get_ylim(), 2))
# 输出: [16.8 -0.8]            ← 大值在前
plt.show()

翻转 Y 的两种写法

Python14 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(5), np.arange(5) ** 2)
ax.set_ylim(16, -0.8)            # 显式给反序范围,最明确
print(np.round(ax.get_ylim(), 2))
# 输出: [16.  -0.8]

fig2, ax2 = plt.subplots()
ax2.plot(np.arange(5), np.arange(5) ** 2)
ax2.set_ylim(reversed(ax2.get_ylim()))    # 按当前范围反向,不用记数字
print(np.round(ax2.get_ylim(), 2))
# 输出: [16.8 -0.8]

set_ylim(bottom, top) 里 bottom > top 就是反向——名字叫 bottom/top,但位置已经不重要了,只看数值顺序。

invert_yaxis() 是「翻转当前状态」:连调两次会回到原样。要「一定是反向」用 set_ylim(大, 小)。

axis('equal') vs set_aspect('equal')

Python14 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot([0, 1, 1], [0, 0, 1])
ax.set_aspect('equal')          # 1 单位 x 和 1 单位 y 一样长
print(ax.get_aspect())
# 输出: 1.0

fig2, ax2 = plt.subplots()
ax2.plot([0, 1, 1], [0, 0, 1])
ax2.axis('equal')               # 等价于 set_aspect('equal', adjustable='datalim')
print(ax2.get_aspect())
# 输出: 1.0

两者 get_aspect() 都返回 1.0,差别在 adjustable:set_aspect('equal') 默认 adjustable='box'(缩框),axis('equal') 用 'datalim'(改数据范围去凑比例)。画地图、画圆时 'box' 更稳;想保住给定的 xlim/ylim 用 'datalim'。

set_box_aspect:框的物理比例

Python9 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots(figsize=(6, 3))
ax.plot(np.arange(5))
ax.set_box_aspect(2)             # 框高 = 框宽 × 2
print(ax.get_box_aspect())
# 输出: 2.0
plt.show()

set_box_aspect(2) 之后,哪怕 figsize=(6, 3),这个 axes 也会被改成竖长条——它会覆盖 figsize 里给的比例。

注意事项

  • invert_yaxis() 是切换(toggle),连调两次等于没调。
  • set_ylim(16, -0.8) 与 invert_yaxis() 视觉一致,但前者可重复执行、后者不行。
  • axis('equal') 会改数据范围(adjustable='datalim'),你之前设的 xlim 可能被覆盖。
  • set_aspect('equal') 默认改框(adjustable='box'),xlim 保住但图变窄。
  • set_box_aspect 与 set_aspect 互不干扰,可以同时用。
  • 3.10 没有 set_xinverted() / get_xinverted()(3.11 才加),判断反向用 ax.xaxis_inverted()。

隐藏坐标轴:五个层次

「隐藏坐标轴」有五个粒度,从「全不要」到「只去掉一条框线」,选错就会出现「刻度数字还在」「框线还在」这种半成品效果。

Python7 行
Axes.set_axis_off()                          # ① 最狠:轴对象整个不画
Axes.xaxis.set_visible(False)                # ② 只隐藏 x 轴(含刻度和标签)
Axes.tick_params(left=False, labelleft=False,
                 bottom=False, labelbottom=False)   # ③ 只隐藏刻度线和刻度文字
Axes.set_xticks([])                          # ④ 等价:清空刻度位置
Axes.spines["top"].set_visible(False)        # ⑤ 只隐藏某条框线
Axes.set_axisbelow(True)                     # 网格放到数据下面
做法 隐藏了什么 保留了什么
set_axis_off() 框线、刻度、刻度文字、轴标签 数据、标题、图例
xaxis.set_visible(False) 该轴全部 另一轴、数据
tick_params(...=False) 刻度线 + 刻度文字 框线、轴标签
set_xticks([]) 刻度位置(连带文字) 框线、轴标签
spines["x"].set_visible(False) 一条框线 该轴的刻度

最小示例

Python7 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(6), np.arange(6) ** 1.5, 'o-')
ax.set_axis_off()                # 整块轴都不要
plt.show()

五种隐藏层次的对比

① set_axis_off():全隐藏

Python8 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(6), np.arange(6) ** 1.5)
ax.set_axis_off()
ax.set_title('标题还在')
plt.show()

标题、图例、text() 都还在,只有「轴」这一整套没了。要恢复用 ax.set_axis_on()。

② xaxis.set_visible(False):只杀一个方向

Python7 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(6), np.arange(6) ** 1.5, 'o-')
ax.xaxis.set_visible(False)
plt.show()

ax.xaxis / ax.yaxis 是 XAxis / YAxis 对象,set_visible(False) 会把刻度、刻度文字、轴标签一起关掉,但下方那条 spine 不受影响——它是 ax.spines['bottom'],属于另一个对象。

③ tick_params:只关刻度,留框线

Python8 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(6), np.arange(6) ** 1.5, 'o-')
ax.tick_params(left=False, labelleft=False,
               bottom=False, labelbottom=False)
plt.show()

四个开关各管一件事:left / bottom 是刻度线,labelleft / labelbottom 是刻度文字。只关 labelleft 就保留了短刻度线但没数字——这是「保持框完整、去掉数字噪音」的常用做法。

④ set_xticks([]):清空刻度位置

Python10 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(6), np.arange(6) ** 1.5, 'o-')
ax.set_xticks([])
ax.set_yticks([])
print(len(ax.get_xticks()))
# 输出: 0
plt.show()

效果和 tick_params(...=False) 接近,但同时删掉了刻度位置信息——后面 ax.text 若依赖 get_xticks() 会拿到空数组。

⑤ spines:只去掉一条框线

Python10 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(6), np.arange(6) ** 1.5, 'o-')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
print([k for k, s in ax.spines.items() if s.get_visible()])
# 输出: ['left', 'bottom']
plt.show()

spines 是一个只读字典,键固定 'left' / 'right' / 'top' / 'bottom'。3.11 起支持切片 ax.spines[:](3.10 也实测可用),可以一次遍历。

set_axisbelow:网格与数据的层级

Python10 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(6), np.arange(6) ** 1.5, 'o-')
ax.grid(True)
print(ax.get_axisbelow())
# 输出: line
ax.set_axisbelow(True)      # 轴和网格压到数据下面
plt.show()

get_axisbelow() 默认返回 'line'(在线的下面、patch 的上面)。设 True 等价 'line',设 'stamp' 是「压在所有东西下面」,设 False 会让网格盖住数据。

注意事项

  • set_axis_off() 不影响标题、图例、text()、table()——它们不是「轴」的一部分。
  • xaxis.set_visible(False) 不会隐藏下方那条 spine。
  • tick_params(left=False) 只关刻度线;要连数字一起去掉必须再给 labelleft=False。
  • set_xticks([]) 会清掉刻度位置,之后 get_xticks() 返回空数组。
  • spines 的键只有四个,拼错(如 'Left')会 KeyError。
  • set_axisbelow('stamp') 才是真正「网格在一切之下」;True 只压到线下面,柱状图仍会盖住网格。