需求:如何实现像Excel透视图的多层级标签
创建示例数据集
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] #显示中文
plt.rcParams['axes.unicode_minus'] = False #正常显示负号
data = {
'col1': ['X', 'X', 'Y', 'Y', 'Y', 'Z'],
'col2': ['A', 'A', 'A', 'B', 'B', 'B'],
'col3': ['a', 'b', 'c', 'c', 'b', 'c'],
'value': [1, 2, 3, 4, 5, 6]
}
df = pd.DataFrame(data)
print(df)
方法一:换行符拼接
# 图形基础设置
dpi = 96
img_wdt = 1200
img_hgt = 600
fig = plt.figure(figsize=(img_wdt / dpi, img_hgt / dpi), dpi=dpi)
ax = fig.add_subplot(111)
plt.title("销售情况", fontdict=dict(fontsize=11, fontweight='bold', color='black'), backgroundcolor='yellow', loc='left')
# X轴设置
df['x轴标签'] = df.apply(lambda x: "\n".join([x['col3'], str(x['col2']), x['col1']]), axis=1)
xaxis_n = np.arange(len(df)) # x轴索引列表
plt.xticks(xaxis_n, fontsize=9, rotation=0)
ax.set_xticklabels(list(df['x轴标签']))
ax.bar(xaxis_n, df['value'], label='销量', color=['#0270C1'] * len(df))
plt.show()
缺点:一定程度上实现了,但是相同的分组没有合并。
方法二:数据标签
所以内置的X轴标签方法无法实现合并效果,只能通过数据标签的形式实现。
思路:
- 最后一层不用合并
- 计算出每个层级的分割线的纵向长度,也决定该层级标签的Y轴位置,长度可以设置在Y轴范围长度的百分之几(3%)
- matplotlib无法在Y轴范围之外添加文本和辅助线,准确的说是添加了看不到,因此只能将X轴标签的区域纳入Y轴范围。
实现代码:
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # 显示中文
plt.rcParams['axes.unicode_minus'] = False # 正常显示负号
data = {
'col1': ['X', 'X', 'Y', 'Y', 'Z', 'Z'],
'col2': ['A', 'A', 'A', 'B', 'B', 'B'],
'col3': ['a', 'b', 'c', 'c', 'c', 'c'],
'value': [1, 2, 3, 4, 5, 6]
}
df = pd.DataFrame(data)
df['x轴标签'] = df.apply(lambda x: "\n".join([x['col3'], str(x['col2']), x['col1']]), axis=1)
df = df.reset_index()
df['x轴序号'] = df.index
print(df)
# 图形基础设置
dpi = 96
img_wdt = 1200
img_hgt = 600
fig = plt.figure(figsize=(img_wdt / dpi, img_hgt / dpi), dpi=dpi)
ax = fig.add_subplot(111)
plt.title("销售情况", fontdict=dict(fontsize=11, fontweight='bold', color='black'), backgroundcolor='yellow', loc='left')
y_series = df['value']
y_min, y_max = min(y_series), max(y_series)
ylim_min = 0
ylim_max = (y_max - ylim_min) * 1.2
xaxis_n = list(df['x轴序号']) # x轴索引列表
bar_width = 0.7 # 柱子宽度
bar_interval = (0.5 - bar_width / 2) * 2
xlim_min = min(xaxis_n) - bar_width / 2 - (1 / 2 - bar_width / 2)
xlim_max = max(xaxis_n) + bar_width / 2 + (1 / 2 - bar_width / 2)
plt.xlim(xlim_min, xlim_max)
# X轴设置
split_line_height = (y_max - ylim_min) * 0.07 # x轴标签每层分割线的高度
group_cols = ['col1', 'col2', 'col3']
ylim_min2 = ylim_max - (ylim_max - ylim_min + len(group_cols) * split_line_height)
plt.ylim(ylim_min2, ylim_max)
plt.axhline(y=ylim_min, c="black", ls="-", lw=1) # 添加X轴水平线
print(xaxis_n)
plt.xticks(xaxis_n, fontsize=9, rotation=0)
ax.set_xticklabels([]) # 原始标签置空
ax.xaxis.set_visible(False)
ax.spines['bottom'].set_visible(False) # 隐藏图形下方边框
for index, row in df.iterrows():
if row['x轴序号'] == 0:
col1_value = {'loc': row['x轴序号'], 'val': row['col1']}
col2_value = {'loc': row['x轴序号'], 'val': row['col2']}
else:
if row['col1'] != col1_value['val']:
text = col1_value['val']
text_x = (col1_value['loc'] + row['x轴序号'] - 1) / 2
ax.text(text_x, ylim_min - split_line_height * 5 / 2, text, ha='center', va='center', fontsize=9,
color='black') # 数据标签
plt.vlines(row['x轴序号'] - bar_width / 2 - bar_interval / 2, ylim_min, ylim_min - 3 * split_line_height,
color='red', linestyles='solid', alpha=1) # 添加左侧分割线
col1_value = {**col1_value, 'loc': row['x轴序号'], 'val': row['col1']}
# 也要更新col2,因为col1分割的点col2必需要分
text = col2_value['val']
text_x = (col2_value['loc'] + row['x轴序号'] - 1) / 2
ax.text(text_x, ylim_min - split_line_height * 3 / 2, text, ha='center', va='center', fontsize=9,
color='black') # 数据标签
plt.vlines(row['x轴序号'] - bar_width / 2 - bar_interval / 2, ylim_min, ylim_min - 2 * split_line_height,
color='blue', linestyles='solid', alpha=1) # 添加左侧分割线
col2_value = {**col2_value, 'loc': row['x轴序号'], 'val': row['col2']}
elif row['col2'] != col2_value['val']:
text = col2_value['val']
text_x = (col2_value['loc'] + row['x轴序号'] - 1) / 2
ax.text(text_x, ylim_min - split_line_height * 3 / 2, text, ha='center', va='center', fontsize=9,
color='black') # 数据标签
plt.vlines(row['x轴序号'] - bar_width / 2 - bar_interval / 2, ylim_min, ylim_min - 2 * split_line_height,
color='blue', linestyles='solid', alpha=1) # 添加左侧分割线
col2_value = {**col2_value, 'loc': row['x轴序号'], 'val': row['col2']}
if index == df.shape[0] - 1: # 最后一行
# 第一层
text = row['col1']
text_x = (col1_value['loc'] + row['x轴序号']) / 2
ax.text(text_x, ylim_min - split_line_height * 5 / 2, text, ha='center', va='center', fontsize=9,
color='black') # 数据标签
# 第二层
text = row['col2']
text_x = (col2_value['loc'] + row['x轴序号']) / 2
ax.text(text_x, ylim_min - split_line_height * 3 / 2, text, ha='center', va='center', fontsize=9,
color='black') # 数据标签
# 最后一层标签
ax.text(row['x轴序号'], ylim_min - split_line_height / 2, row['col3'], ha='center', va='center', fontsize=9,
color='black') # 数据标签
plt.vlines(row['x轴序号'] + bar_width / 2 + bar_interval / 2, ylim_min, ylim_min - split_line_height, color='red',
linestyles='solid', alpha=1) # 添加右侧分割线
ax.bar(xaxis_n, df['value'], label='value', color=['#0270C1'] * len(df), width=bar_width)
plt.show()