You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Matplotlib中设置水平条形图边缘不扩展条形尺寸

问题:Matplotlib水平条形图的linewidth适配与内部绘制问题

背景

我正在开发一个基于Matplotlib的商业图表Python包,希望添加一个受IBCS启发的水平条形图功能。

问题描述

当前实现中,barheight是相对于类别间距的相对值,但linewidth是绝对值——边缘线会绘制在条形外部,导致实际占比空间变大。在大尺寸图表中,条形间仍有空白;但用相同参数创建小尺寸图表时,条形间空白消失,甚至出现前后条形互相遮挡的情况。

复现代码

import matplotlib.pyplot as plt
from random import *

linewidth = 2
barheight = 0.65

# 创建y坐标,预算条形略高于实际条形
y_budget = [y+0.1*barheight for y in range(10)]
y_actual = [y-0.1*barheight for y in range(10)]

# 生成随机条形长度
width_budget = [randint(1,100) for w in range(10)]
width_actual = [randint(1,100) for w in range(10)]

# 大尺寸图表
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 6))
ax.barh(y=y_budget, width=width_budget, color='#FFFFFF', height=barheight, linewidth=linewidth, edgecolor='#404040')
ax.barh(y=y_actual, width=width_actual, color='#404040', height=barheight, linewidth=linewidth, edgecolor='#404040')
ax.set_yticks(y_actual, ["category "+str(i+1) for i in range(10)]);

# 小尺寸图表
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 2))
ax.barh(y=y_budget, width=width_budget, color='#FFFFFF', height=barheight, linewidth=linewidth, edgecolor='#404040')
ax.barh(y=y_actual, width=width_actual, color='#404040', height=barheight, linewidth=linewidth, edgecolor='#404040')
ax.set_yticks(y_actual, ["category "+str(i+1) for i in range(10)]);

解决方案

方案1:将边缘线绘制在条形内部

Matplotlib的barh没有直接设置边缘线向内的参数,但可以通过双层条形模拟内部边框:底层绘制稍大的边框色条形,上层绘制稍小的填充色条形,实现边框在内部的视觉效果。

import matplotlib.pyplot as plt
from random import randint

barheight = 0.65
# 填充条形的高度比边框条形小,留出边框空间
border_height = barheight
fill_height = barheight - 0.2

# 数据准备
y_budget = [y+0.1*barheight for y in range(10)]
y_actual = [y-0.1*barheight for y in range(10)]
width_budget = [randint(1,100) for w in range(10)]
width_actual = [randint(1,100) for w in range(10)]

# 小尺寸图表测试
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 2))

# 先绘制底层边框条形
ax.barh(y=y_budget, width=width_budget, color='#404040', height=border_height, linewidth=0)
ax.barh(y=y_actual, width=width_actual, color='#404040', height=border_height, linewidth=0)

# 再绘制上层填充条形
ax.barh(y=y_budget, width=width_budget, color='#FFFFFF', height=fill_height, linewidth=0)
ax.barh(y=y_actual, width=width_actual, color='#404040', height=fill_height, linewidth=0)

ax.set_yticks(y_actual, ["category "+str(i+1) for i in range(10)]);

方案2:让linewidth随图表尺寸相对变化

由于linewidth是像素单位,barheight是轴坐标单位,可通过图表像素与轴坐标的转换比例,动态计算适配的linewidth,保持其相对于barheight的比例固定。

import matplotlib.pyplot as plt
from random import randint

barheight = 0.65
# 设定linewidth占barheight的比例(例如5%)
linewidth_ratio = 0.05

# 数据准备
y_budget = [y+0.1*barheight for y in range(10)]
y_actual = [y-0.1*barheight for y in range(10)]
width_budget = [randint(1,100) for w in range(10)]
width_actual = [randint(1,100) for w in range(10)]

# 小尺寸图表测试
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 2))

# 计算轴坐标与像素的转换比例
y_range = ax.get_ylim()[1] - ax.get_ylim()[0]
pixel_height = fig.dpi * fig.get_figheight()
pixels_per_y = pixel_height / y_range

# 动态计算适配的linewidth
linewidth = barheight * linewidth_ratio * pixels_per_y

# 绘制条形
ax.barh(y=y_budget, width=width_budget, color='#FFFFFF', height=barheight, linewidth=linewidth, edgecolor='#404040')
ax.barh(y=y_actual, width=width_actual, color='#404040', height=barheight, linewidth=linewidth, edgecolor='#404040')

# 修正绘制后轴范围变化带来的比例偏差
ax.set_ylim(-0.5, 9.5)
y_range = ax.get_ylim()[1] - ax.get_ylim()[0]
pixels_per_y = pixel_height / y_range
linewidth = barheight * linewidth_ratio * pixels_per_y

# 更新所有条形的linewidth
for patch in ax.patches:
    patch.set_linewidth(linewidth)

ax.set_yticks(y_actual, ["category "+str(i+1) for i in range(10)]);

方案3:手动绘制矩形精确控制边框

使用matplotlib.patches.Rectangle手动绘制条形,通过坐标转换将linewidth的像素单位转为轴坐标,精确调整矩形位置和尺寸,让边缘线完全在条形内部。

import matplotlib.pyplot as plt
from random import randint
from matplotlib.patches import Rectangle

barheight = 0.65
linewidth = 2  # 像素单位

# 数据准备
y_budget = [y+0.1*barheight for y in range(10)]
y_actual = [y-0.1*barheight for y in range(10)]
width_budget = [randint(1,100) for w in range(10)]
width_actual = [randint(1,100) for w in range(10)]

# 小尺寸图表测试
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 2))

# 将像素单位的linewidth转换为轴坐标单位
trans = ax.transData
pix_to_y = trans.inverted().transform((0, linewidth))[1] - trans.inverted().transform((0, 0))[1]

# 绘制预算条形(白填充+灰边框)
for y, w in zip(y_budget, width_budget):
    rect = Rectangle(
        (0, y - barheight/2 + pix_to_y), 
        w, 
        barheight - 2*pix_to_y,
        facecolor='#FFFFFF', 
        edgecolor='#404040', 
        linewidth=linewidth
    )
    ax.add_patch(rect)

# 绘制实际条形(灰填充+灰边框)
for y, w in zip(y_actual, width_actual):
    rect = Rectangle(
        (0, y - barheight/2 + pix_to_y), 
        w, 
        barheight - 2*pix_to_y,
        facecolor='#404040', 
        edgecolor='#404040', 
        linewidth=linewidth
    )
    ax.add_patch(rect)

# 设置轴范围
ax.set_ylim(-0.5, 9.5)
ax.set_xlim(0, max(max(width_budget), max(width_actual)) + 10)
ax.set_yticks(y_actual, ["category "+str(i+1) for i in range(10)]);

内容的提问来源于stack exchange,提问作者MarcelW1323

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 15:47:06