一根柱子的高度顶到天花板,剩下七根压在地板上几乎看不出差别——这是柱状图最常见、也最让人头疼的一种失效方式。数值本身没错,但图的表达力被一个极端值吃掉了。坐标轴加"break"的柱状图,也就是通常说的断轴柱状图、轴断裂柱状图,解决的就是这类量级失衡问题:把纵轴在某个空档处切断,上半段放大高位区间,下半段放大低位区间,让每一根柱子都能被看清。这套做法在实验数据、运营报表、性能基准测试里出现频率很高,但真正画对的人并不多——断口切在哪里、两段比例尺差多少会失真、断裂符号画多大、柱子跨过断口怎么处理,每一步都有讲究。下面这些内容来自我自己反复改图、被审稿意见打回来重画之后攒下的经验,适合已经会画普通柱状图、但被长尾数据卡住的人看,也适合刚接触可视化、想少走弯路的新手直接对照着抄。
1. 一根柱子站得太高,其余全部趴在地上
1.1 量级失衡把柱状图变成了"一根火柴加一排地板"
先说清楚失效是怎么发生的。假设八组数据里七组在 9 到 15 之间,剩下一组是 96.5。按线性轴画出来,图高 400 像素对应 0 到 100,那么一根高度 12 的柱子只有 48 像素,一根高度 9.8 的柱子是 39 像素,两者差 9 个像素。这 9 个像素在屏幕上大概就是一根头发的宽度,读者根本分不出来谁高谁低。更别说七根柱子挤在一起,视觉上完全是一块等高的色块,只有那根 96.5 的柱子立在那里。
这种图的问题不在于"不好看",而在于它传递的信息量被压缩到了接近零。你辛辛苦苦采了八组数据,最后读者只能看出"E 组特别大",其余七组的信息全部丢失。这在数据汇报里是硬伤——被问一句"B 组和 G 组差多少",你只能翻原始表格念数字。
常规的补救思路有两条。一条是把柱子改成横向条形图,但这只是换了方向,量级失衡的问题原封不动。另一条是加数据标签,在每根柱子顶上标数值,让人靠读数字而不是看高度来比较。数据标签能救急,但它把一张"图"降级成了一张"带颜色的表",读者失去了靠视觉直觉抓差异的能力,而且柱子一多标签就打架。
断轴走的是第三条路:承认数据本身有两个量级层次,那就用两个不同的纵轴刻度去承接,中间用断裂符号标出来。低位区间(比如 0 到 20)占用图的大部分高度,高位区间(比如 80 到 100)占一小部分,极端值那根柱子被压缩在上方那一小块里,但依然可见;下面七根柱子则获得了原本几倍的高度空间,彼此之间的差异一下子清晰了。
需要注意的是,这种做法的本质是牺牲大值的视觉权重,换取小值的可分辨性,方向和我们直觉上想的可能相反。很多人第一反应是"断轴是不是会把大的显得更大",其实恰恰相反,它是把大值砍短。理解了这一点,后面调比例尺的时候就不会犯方向性错误。
1.2 断轴、对数轴、双轴刻度:三条路的边界在哪
遇到量级失衡,可选的方案不止断轴一种。我通常按下面这个顺序去判断,先用最简单的,不行再往上加复杂度。
| 方案 | 适用条件 | 优点 | 代价 |
|---|---|---|---|
| 线性轴 + 数据标签 | 数据量少于 10 组,读者只关心具体数值 | 最直观,无需额外说明 | 视觉比较能力丧失 |
| 对数轴 | 数据跨越 2 个数量级以上,且关心的是倍数关系 | 一根轴搞定,不破坏连续性 | 零值和负值无法处理,读者普遍不习惯 |
| 断轴 | 数据呈"主体 + 少数离群"的两簇结构,两簇内部都关心排序 | 两段各自线性,读者熟悉 | 视觉比例失真,需要额外标注 |
| 双轴(左右两个 y 轴) | 两组不同量纲的指标放在一起对比 | 能同时展示两种单位 | 极易被误读成相关关系,误导风险最高 |
对数轴和断轴的适用条件有一个关键区别:对数轴适合"数据本身服从幂律分布、且你关心的是增长率"的场景,比如用户数从 100 涨到 10000。断轴适合"数据分成两簇、你希望在每一簇内部做精细比较"的场景,比如上面那种七小一大的结构。如果你的数据里有 0,对数轴直接出局;如果数据只有一根柱子特别高、其余都差不多,断轴会让那根高柱子显得很别扭,因为它被切成了孤零零的两截。
双轴我基本不推荐用在这种场景。左右两个轴各有一套刻度,读者第一眼根本分不清哪根柱子该读哪个轴,用来做对比图的时候尤其容易出事。除非两边的单位完全不同(比如柱状图是销量、折线图是转化率),否则不要碰。
还有一个常被忽略的选项:干脆把那张图拆成两张。如果高位区间和低位区间分别只有寥寥几个数据点,与其在一张图上动刀子,不如画两张共享 x 轴、纵轴独立的小图,中间留白,配一句"上图纵轴范围为 0–20,下图为 80–100"。这种"分面"的做法在正式报告和论文里其实比断轴更稳妥,因为它完全没有失真的问题。断轴的价值主要在于——它保持了一根连续的轴,读者不会觉得这是两张图拼起来的,心理上更容易接受"这是一组数据"。
1.3 断口切在哪:一个能落地的判断方法
断口位置是整个断轴图里最容易凭感觉乱来的地方,但它其实有一套可以量化的判断流程。设数据从小到大排序后为 $x_1 \le x_2 \le \cdots \le x_n$,把"下方最大值"记为 $L$,把"上方最小值"记为 $U$。断口区间是 $[b_1, b_2]$,其中 $b_1 \ge L$,$b_2 \le U$。
第一步,判断要不要断。计算 $U / L$ 的比值。如果这个比值小于 5,说明数据的最大最小差不太多,用普通线性轴加上合适的高度就能看清,没必要断。比值在 5 到 15 之间属于灰色地带,可以先试试调整图表长宽比、或者改成横向条形图。比值大于 15,断轴的优势才开始明显。上面那组数据的比值是 96.5 / 14.1 ≈ 6.8,属于"边缘案例",但因为下面七个数挤得太紧(9.8 到 14.1,跨度只有 4.3),所以断轴依然划算。
第二步,定 $b_1$。取下界是"下方最大值往上留一点余量",公式是 $b_1 \approx 1.2 \sim 1.5 \times L$,并且向上取整到一个整齐的刻度值。上例中 $1.3 \times 14.1 = 18.3$,向上取整到 20,很合适。
第三步,定 $b_2$。取上界是"上方最小值往下留一点余量",公式是 $b_2 \approx 0.75 \sim 0.85 \times U$,向下取整到整齐刻度。上例中 $0.8 \times 96.5 = 77.2$,向下取整到 80。这样断口就是 20 到 80,跨度 60。
第四步,验证断口宽度是否合理。断口跨度 $b_2 - b_1$ 最好不要小于下方数据范围的 1.5 倍。这里下方数据范围是 0 到 20,跨度 20,断口跨度 60,是它的 3 倍,视觉上够明显,读者一眼能看出"这里跳了"。
如果数据是长尾结构、没有明显的两簇,可以改用分位数来定:把 90% 分位或 95% 分位作为 $L$ 的基准,把最大值单拎出来放在上段。这样做的前提是你明确知道自己在"牺牲少数点、保留主体",需要在图注里说清楚。
还有一条硬性约束:断口区间里最好不要有任何数据点。如果有一根柱子的值恰好落在断口内,它就会在图上彻底消失——既不在下段也不在上段,这是断轴最常见的 bug。定完 $b_1$ 和 $b_2$ 之后,务必扫一遍数据,确认没有被"吞掉"的值。
2. matplotlib 里的断轴骨架:两个 Axes 拼出一根轴
2.1 GridSpec 的高度分配决定了"哪段更值钱"
matplotlib 没有原生的断轴功能,官方示例给出的思路是"画两个子图,上下堆叠,把中间那条接缝伪装成不存在"。这个方案听起来土,但它给了你完全的控制权,比第三方封装库灵活得多。
先看骨架代码:
import numpy as np import matplotlib.pyplot as plt from matplotlib.gridspec import GridSpec labels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] values = np.array([12.4, 9.8, 14.1, 11.2, 96.5, 8.7, 13.3, 10.6]) fig = plt.figure(figsize=(7.5, 5.5), dpi=150) gs = GridSpec(2, 1, height_ratios=[1, 2.4], hspace=0.06) ax_top = fig.add_subplot(gs[0]) ax_bot = fig.add_subplot(gs[1], sharex=ax_top) for ax in (ax_top, ax_bot): ax.bar(np.arange(len(values)), values, width=0.62, color='#4C72B0', edgecolor='white', linewidth=0.8, zorder=3) ax_top.set_ylim(80, 100) ax_bot.set_ylim(0, 20) ax_top.set_xlim(-0.7, len(values) - 0.3)height_ratios=[1, 2.4]是这张图的灵魂。它的含义是:上段占 1 份高度、下段占 2.4 份。上段覆盖 80 到 100,跨度 20 个单位;下段覆盖 0 到 20,跨度也是 20 个单位。所以下段的"每单位像素"是上段的 2.4 倍——低位区间的柱子被放大了 2.4 倍,这才是断轴想要的效果。
如果反过来写成[2.4, 1],上段就会被放大,结果是极端值那根柱子变得又粗又长,低位柱子反而更矮。这个方向搞反了,整张图的意义就没了,我第一次画的时候就栽在这儿,盯着图看了半天觉得"怎么断完还是看不清"。
sharex=ax_top保证两个子图的 x 轴完全同步,柱子宽度、位置自动一致,不需要手动对齐。hspace=0.06是接缝的间距,取值太小两段会粘在一起看不出断裂,太大又会显得像两张独立的图,0.05 到 0.10 之间比较合适。
还有一行容易漏的:两个子图里都画了完整的bar。上子图里值 12.4 的柱子因为ylim是 80 到 100,会被完全裁掉;下子图里值 96.5 的柱子会被裁到 20 为止。这正好是我们想要的——同一根高柱子被切成上下两截,低位柱子在上段不可见、在下段完整显示。matplotlib 的裁剪默认是开启的(clip_on=True),不用额外设置。
2.2 接缝处理的四个开关:脊线、刻度、网格、标签
画完两个子图,接缝处会留下两条平行的横线,一看就是"两张图拼的"。要把它伪装成一根轴,需要处理四个地方。
第一条:隐藏相邻的脊线。上子图的底边和子图的上边都要关掉。
ax_top.spines['bottom'].set_visible(False) ax_bot.spines['top'].set_visible(False)第二条:清掉上子图的 x 轴刻度和标签。因为sharex之后两个子图的 x 刻度都会显示,接缝处会出现两排标签叠在一起。
ax_top.tick_params(axis='x', which='both', bottom=False, labelbottom=False, top=False, labeltop=False)第三条:处理 y 轴刻度密度。两段的刻度间距要看起来差不多,否则一眼假。上段跨 20 单位、下段也跨 20 单位,但上段只有 0.29 个图高,下段有 0.71 个图高。如果两段都用 5 作为刻度间隔,上段会显得很挤。解决办法是让上段的刻度稀疏一点:
ax_top.set_yticks([80, 90, 100]) ax_bot.set_yticks([0, 5, 10, 15, 20])这样上段三个刻度、下段五个刻度,视觉密度接近。
第四条:网格线的处理。横向网格线是画在各自子图内部的,天然不会跨过接缝,所以直接开就行。但要注意 zorder,网格必须画在柱子下面:
for ax in (ax_top, ax_bot): ax.grid(axis='y', linestyle=':', color='#cfcfcf', linewidth=0.8, zorder=0) ax.set_axisbelow(True)set_axisbelow(True)是保险措施,它让网格线始终位于数据之下。少了这行,在某些样式下网格会压在柱子上,看起来很脏。
还有一个小细节:fig.align_ylabels([ax_top, ax_bot])可以让两个子图的 y 轴标签左对齐,避免因为刻度位数不同(一个是两位数、一个是三位数)导致标签位置错位。
2.3 断口符号的坐标计算与画法
接缝处理完之后,两段之间只剩下一条空白带,读者不知道这里发生了什么。断裂符号就是告诉读者"轴在这里断了"的视觉标记。学术图表的惯例是在轴的左右两端各画一对平行斜线,形如//,也有画成锯齿状的。
在 matplotlib 里画这个,用plot配合transAxes坐标系最方便。transAxes的原点在轴框左下角、右上角是 (1, 1),所以不管数据范围怎么变,斜线永远贴在图框边缘。
d = 0.014 # 斜线的半跨度,相对于轴宽的比值 kw = dict(color='black', linewidth=1.1, clip_on=False, zorder=6) # 左侧斜线 ax_top.plot((-d, d), (-d, d), transform=ax_top.transAxes, **kw) ax_bot.plot((-d, d), (1 - d, 1 + d), transform=ax_bot.transAxes, **kw) # 右侧斜线 ax_top.plot((1 - d, 1 + d), (-d, d), transform=ax_top.transAxes, **kw) ax_bot.plot((1 - d, 1 + d), (1 - d, 1 + d), transform=ax_bot.transAxes, **kw)这里ax_top的y=-d对应的是上子图的底边,ax_bot的y=1-d对应的是下子图的顶边。两条斜线在接缝处交叉,形成一个"X"或"八"的形状,配上hspace的留白,视觉上就像是轴被切断后留下的断口。
d的取值在 0.010 到 0.018 之间比较合适。小于 0.008 会细得看不见,尤其在缩小后;大于 0.025 会画成一个很大的叉,喧宾夺主。如果你的图要印在纸上,建议取 0.015 左右。
clip_on=False是关键,不加这行斜线会被裁在轴框内,只露出一小段。但加了它之后,导出时用bbox_inches='tight'可能会把斜线切掉一点,需要配合pad_inches:
fig.savefig('broken_axis_bar.png', dpi=300, bbox_inches='tight', pad_inches=0.12)也有人喜欢在中间位置再画一组斜线,让整条断裂带更连贯。我的做法是:如果图很宽(超过 8 英寸),中间加一组;窄图就只画左右两端,免得视觉上太闹。
2.4 跨断口的柱体:同一根柱子画两遍
柱状图和折线图断轴有一个本质区别:折线是点对点连接,断口位置随便挑,只要不穿过某个点就行;柱状图的柱子是从 0 基线往上长的,只要柱子的值超过了 $b_1$,它就必然跨过断口。
这意味着每一根高柱子都会在图上被切成两截。上段露出柱头,下段露出柱身,中间有一段空白。这本身不是 bug,但处理不好会很难看。
第一个要注意的是颜色和边框必须完全一致。两个子图的bar调用参数要对齐,color、edgecolor、linewidth、width一个都不能差。我见过有人上下两段用了不同的填充色,看着像两组独立的数据。
第二个是edgecolor的陷阱。如果你的柱子带白色边框,上段柱头的底部会出现一条白线,看起来像是柱头断掉了。解决办法是把边框改细,或者干脆关掉边框:
for ax in (ax_top, ax_bot): ax.bar(np.arange(len(values)), values, width=0.62, color='#4C72B0', edgecolor='none', zorder=3)第三个是断裂边界处的"毛边"。如果 $b_1$ 和 $b_2$ 取得离某个数据值很近,那根柱子的柱头或柱身会只剩下一两个像素,看着像画歪了。这就是前面强调"断口里不能有数据点"的原因——不只是会被吞掉,擦边也会产生视觉噪音。
第四个是柱宽。因为sharex保证了两个子图的 x 轴完全一致,柱宽会自动同步,不需要额外处理。但如果你用的是width参数之外的方式(比如按数据坐标设置宽度),就要确认两边一致。
最后一个小技巧:在下段的顶部加一条淡淡的虚线,标注 $b_1$ 的位置;上段的底部同样标 $b_2$。这在正式图里比断裂符号更能说明问题,因为它直接告诉你"断在哪两个刻度之间"。
3. ggbreak 与 ECharts:把断轴交给现成的组件
3.1 R 里 scale_y_break 的用法与它藏起来的细节
如果你主要用 R,ggbreak这个扩展包把断轴封装成了一行代码,比 matplotlib 那套手工活省事太多。
library(ggplot2) library(ggbreak) df <- data.frame( cat = LETTERS[1:8], val = c(12.4, 9.8, 14.1, 11.2, 96.5, 8.7, 13.3, 10.6) ) p <- ggplot(df, aes(x = cat, y = val)) + geom_col(fill = "#4C72B0", width = 0.62) + scale_y_break(c(20, 80), scales = 0.42, space = 0.06) + scale_y_continuous(expand = expansion(mult = c(0, 0.02))) + theme_classic(base_size = 12) + theme(axis.text.x = element_text(size = 11)) ggsave("bar_break.pdf", p, width = 6.5, height = 4.8)scale_y_break(c(20, 80))直接指定断口为 20 到 80,剩下的排版工作包会自动处理,包括断裂符号的绘制。scales = 0.42控制两段的相对比例——这个参数的含义是"上段相对于下段的缩放系数",取值越大上段越高。默认是 0.5,我一般调到 0.35 到 0.45 之间,让下段占更大比重。space控制断口的留白宽度,默认 0.1,调小到 0.05 左右断口会更紧凑。
它能自动处理的细节包括:断裂符号的两条斜线、y 轴刻度的重新分布、接缝处脊线的隐藏。这些如果用原生 ggplot2 手搓,得写不少annotation_custom和coord_cartesian的组合,很麻烦。
但ggbreak有几个需要提前知道的限制。
限制一:coord_flip()会打架。如果你想把柱状图转成横向,scale_y_break结合coord_flip在不少版本上会渲染异常,断裂符号位置错乱。横向断轴建议改成scale_x_break配合coord_flip,或者干脆用orientation = "y"的geom_col。
限制二:和facet_wrap组合有风险。分面加断轴属于"两层裁切",ggbreak对分面的支持一直不算完善,我实测下来在分面数量超过 2 的时候容易出现某几个分面的断裂符号丢失。
限制三:导出 PDF 时字体嵌入。有些版本的ggbreak在 PDF 输出时断裂符号会被画成路径而不是线条,导致在某些 PDF 阅读器里显示为空心。稳妥的做法是先出 PNG 检查一遍,没问题再出 PDF。
限制四:数据点落在断口内不会报错。这是最坑的一点——scale_y_break(c(20, 80))如果有一根柱子的值正好是 50,它会安安静静地消失,没有任何警告。所以每次改断口范围之后,都要人工核对一遍数据。
3.2 ECharts 双 grid 拼轴的配置要点
网页端做断轴柱状图,ECharts 的原生方案是"两个 grid 上下叠放"。它的思路和 matplotlib 一致,但因为配置是 JSON 式的,坐标要靠手算。
const names = ['A','B','C','D','E','F','G','H']; const vals = [12.4, 9.8, 14.1, 11.2, 96.5, 8.7, 13.3, 10.6]; const opt = { animation: false, grid: [ { left: 56, right: 24, top: 22, height: '30%' }, { left: 56, right: 24, top: '48%', height: '40%' } ], xAxis: [ { type: 'category', data: names, gridIndex: 0, axisLine: { show: false }, axisTick: { show: false }, axisLabel: { show: false } }, { type: 'category', data: names, gridIndex: 1, axisLine: { lineStyle: { color: '#999' } }, axisTick: { show: false } } ], yAxis: [ { type: 'value', gridIndex: 0, min: 80, max: 100, interval: 10, axisLine: { show: true, lineStyle: { color: '#999' } }, splitLine: { lineStyle: { type: 'dashed', color: '#eaeaea' } } }, { type: 'value', gridIndex: 1, min: 0, max: 20, interval: 5, axisLine: { show: true, lineStyle: { color: '#999' } }, splitLine: { lineStyle: { type: 'dashed', color: '#eaeaea' } } } ], series: [ { type: 'bar', xAxisIndex: 0, yAxisIndex: 0, data: vals, barWidth: '46%', itemStyle: { color: '#4C72B0' } }, { type: 'bar', xAxisIndex: 1, yAxisIndex: 1, data: vals, barWidth: '46%', itemStyle: { color: '#4C72B0' } } ] };关键点在于series里要写两份,每份通过xAxisIndex/yAxisIndex绑定到对应的坐标系上。两个 grid 的left和right必须完全一致,否则柱子宽度会错位——虽然写的是同一个百分比46%,但百分比是相对各自的 grid 宽度算的,grid 宽度不同,实际像素宽度就不同。
上段 grid 的height: '30%'和下段height: '40%'决定了比例。这里上段覆盖 20 单位、下段覆盖 20 单位,但上段高度占比更小,所以下段的每单位像素更高,符合断轴的本意。
断裂符号 ECharts 没有内置,得靠graphic手绘:
graphic: [ { type: 'line', left: 50, top: 218, shape: { x1: 0, y1: 0, x2: 9, y2: 9 }, style: { stroke: '#555', lineWidth: 1.2 } }, { type: 'line', left: 50, top: 226, shape: { x1: 0, y1: 0, x2: 9, y2: 9 }, style: { stroke: '#555', lineWidth: 1.2 } }, { type: 'line', right: 16, top: 218, shape: { x1: 0, y1: 0, x2: 9, y2: 9 }, style: { stroke: '#555', lineWidth: 1.2 } }, { type: 'line', right: 16, top: 226, shape: { x1: 0, y1: 0, x2: 9, y2: 9 }, style: { stroke: '#555', lineWidth: 1.2 } } ];这里的top是硬编码的像素值,非常脆弱——容器高度一变、grid 的百分比一变,斜线就跑到别的地方去了。比较稳的做法是先把 grid 的 top 和 height 换算成像素,用一个函数统一算断口位置:
function breakMarkYs(containerH, topPct, heightPct) { const gapTop = containerH * topPct / 100; const gapBottom = containerH * (parseFloat(topPct.replace('%','')) + parseFloat(heightPct.replace('%',''))) / 100; return { upper: gapTop, lower: gapBottom }; }实际项目里我干脆把 grid 的 top 和 height 都改成数字像素,避免百分比换算带来的不确定性。图表容器尺寸固定的时候,这样最省心。
ECharts 方案的优势是交互——可以挂 tooltip,鼠标悬停时显示原始数值,这在一定程度上缓解了断轴造成的读图困难。但要注意 tooltip 的 formatter 里两份 series 会各触发一次,需要去重。
3.3 四套方案的实测对照
我把几种常见实现方式放在一起对比,方便你按项目情况挑。
| 方案 | 断裂符号 | 柱子跨断口 | 学习成本 | 适合场景 |
|---|---|---|---|---|
| matplotlib 双子图 | 手画斜线,完全可控 | 自动裁剪,需注意边框 | 中,代码约 30 行 | 论文、出版级静态图 |
| ggplot2 + ggbreak | 自动生成 | 自动裁剪 | 低,一行参数 | 快速出图的统计分析 |
| ECharts 双 grid | 手绘 graphic | 各画一份,需绑定轴 | 中高,配置冗长 | 网页仪表盘、交互展示 |
| 第三方 brokenaxes 库 | 自动生成 | 对 bar 支持弱 | 低 | 折线图为主,柱状图慎用 |
brokenaxes那个库要单独说一句。它确实能一行搞定断轴,但它是按"数据坐标裁剪"的思路设计的,对bar的支持一直不太好——柱子的基线是 0,被裁到上段之后底部会悬空,看起来像漂浮的色块。如果是折线图或散点图,用它是可以的;柱状图我建议还是老老实实拼子图。
4. 断轴最容易被质疑的地方,恰恰不是代码
4.1 断裂比例失真是数学事实,只能靠标注弥补
技术实现只是第一步。断轴图真正难的地方在于:它天生就是一张会骗人的图,而你必须想办法把骗人的程度控制住。
先把失真算清楚。设下段每单位数据对应 $p_1$ 像素,上段每单位对应 $p_2$ 像素。前面那组配置里,下段覆盖 0 到 20,占图高的 71%;上段覆盖 80 到 100,占图高的 29%。假设图高 500 像素,则 $p_1 = 500 \times 0.71 / 20 = 17.75$ 像素/单位,$p_2 = 500 \times 0.29 / 20 = 7.25$ 像素/单位。下段的放大倍数是上段的 2.45 倍。
现在看两根柱子:B 组是 9.8,E 组是 96.5。真实比值 9.85 倍。图上的视觉长度呢?B 组在下段,长度 $9.8 \times 17.75 = 174$ 像素;E 组在上段,从 80 起算,长度 $(96.5 - 80) \times 7.25 = 119.6$ 像素。视觉上 B 组反而比 E 组长了接近一半。
这就是断轴的代价:数值大的柱子,画出来的长度反而可能更短。如果读者不看轴刻度、只看柱子长短,会得出完全相反的结论。
知道这一点之后,能做的补救有三条。
第一,图注必须写明断口位置。不能只写"Y 轴已断开",要写清楚断在哪:"纵轴在 20–80 区间断开,下段范围为 0–20,上段为 80–100"。这样读者知道两段的比例尺不同。
第二,在断口处放明显的视觉标记。断裂符号要够大、够显眼,不能画成若隐若现的一条细线。宁可略夸张,也不要让读者忽略。
第三,给高值柱子加数据标签。因为高值柱子的长度已经不可信了,那就直接用数字告诉读者它有多大。低值柱子的长度可信度高,可以不标,或者只标关键几根。
如果要更保守一点,可以把下段的放大倍数控制在 2 倍以内。经验值是:$p_1 / p_2$ 超过 3,读者几乎必然误判;2 到 3 之间需要靠标注辅助;1.5 到 2 之间比较安全,视觉上的"排序"基本还是对的。
还有一个常被忽略的问题:如果断口上下两段里各有一根柱子比较接近,比如下段有 18,上段有 82,这两根柱子在图上可能长度相近,读者会以为它们数值接近。这是断轴最危险的失效模式。遇到这种情况,我的做法是把断口拉窄,让 18 和 82 分别贴近断口的上下沿,视觉上它们就会明显分层。
4.2 断裂符号的尺寸、角度与位置
断裂符号看起来是个小装饰,但它承担着"免责声明"的作用——告诉读者"这里的比例尺变了"。画得不对,整张图的可信度就打折。
尺寸上,斜线的跨度(从轴边缘往里伸的长度)建议是轴宽的 1.5% 到 2%,或者换成更直观的说法:斜线在水平方向的长度大约等于柱子宽度的一半。太短看不出来,太长会盖住柱子。
角度上,标准做法是 45 度。上下两组斜线要平行,不能一个 45 度一个 60 度,那样看起来像画歪了。有些期刊要求画成锯齿状(三个尖角),但实现起来麻烦,45 度平行线是通用度最高的。
位置上有三种常见排布。
其一,只画在 y 轴一侧。这是最简洁的做法,断口信息集中在左边,不干扰柱子的阅读。缺点是如果柱子延伸到右边,右侧的断口就不明显了。
其二,左右两侧都画。这是我更推荐的,尤其是图表比较宽的时候。左右各一对斜线,构成"整条轴被切断"的完整性。
其三,横跨整个宽度画一条带断裂的横线。这种最显眼,适合面向大众的汇报图,但在学术图里略显笨重。
还有一个细节:斜线要贴着轴框的边缘画,而不是画在轴框外面或者里面。画在里面会盖住数据,画在外面会显得漂浮。用transAxes坐标系可以保证贴边,但要注意clip_on=False之后斜线会略微超出轴框,取d = 0.014的时候超出量很小,不影响观感。
如果你的图要黑白打印,斜线用纯黑、线宽 1.0 到 1.2 pt,别用灰色。灰色在小尺寸下会淡到看不见。
4.3 图注、数据标签与配色的一致性约束
一张断轴图要能独立成立,图注的写法有讲究。很多人只写"Y 轴断轴"三个字,这等于没写。完整的写法应该包含三个信息:断口范围、两段各自的刻度范围、断轴的原因。
一个可以参考的模板是:"纵轴在 20–80 区间断开。下段范围 0–20,上段范围 80–100。F 组数值为 96.5,显著高于其余各组,为使组间差异清晰可见采用断轴处理。"
三句话,把该说的都说了,读者不会误判。
数据标签的处理也需要统一。如果给高值柱子标了数值,低值柱子也得标,否则视觉上会失衡。但如果柱子数量很多,全标会挤成一团,那就退一步——只标断口上方的柱子和断口下方最高的那根。这种"有选择的标注"其实比全标更聪明,因为它恰好标在了容易误读的位置。
配色上有一条容易被忽略的约束:跨断口的柱子,上下两段的填充色必须完全一致。因为视觉系统会把"同色"识别为"同一个对象",这样读者才能理解"这是同一根柱子被切成了两截"。如果有人上下用了不同颜色,或者下段带了渐变,读者会以为是两组不同的数据。
另外,断口区域的背景建议保持纯白或透明,不要在那里放网格