如何在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
相关产品推荐
相关产品推荐

