import base64 from io import BytesIO from matplotlib import pyplot as plt from matplotlib.patches import Rectangle from paste import chart from paste.util import ufont def gen_pie(data_df, value_column, percentage_column, legend_labels, color_palette='BuPu', dpi=128): """ 生成环形图(Doughnut Chart),并附带右侧图例(色块 + 文字描述)。 注意:虽然函数名为 gen_pie,但实际绘制的是环形图(有中心空洞),非传统饼图。 参数说明(数据结构要求): -------------------------- data_df : pandas.DataFrame 必须包含以下三列: - value_column (数值列): 每个类别的数值(如设备数量),用于计算扇区角度。 - percentage_column (百分比列): 每个类别的百分比(如 35.2%),用于显示在图例中。 - legend_labels (标签列): 每个类别的名称(如 '服务器', '交换机'),用于图例文本。 示例结构: | server_count | percentage | device_type | |--------------|------------|-------------| | 35 | 35.2% | 服务器 | | 28 | 28.1% | 交换机 | | 22 | 22.0% | 路由器 | 要求: - 所有数值必须 ≥ 0; - 百分比列应为字符串格式(含'%'符号),或可被转换为字符串; - 所有列长度必须一致,且与 data_df 的行数匹配。 value_column : str data_df 中表示数值的列名(如 'server_count')。 percentage_column : str data_df 中表示百分比的列名(如 'percentage'),用于图例中显示。 legend_labels : str data_df 中表示类别名称的列名(如 'device_type'),用于图例文本。 color_palette : str, default='BuPu' Seaborn 颜色调色板名称,用于为每个扇区分配颜色。 可选值参考:'BuPu', 'viridis', 'plasma', 'Set3' 等。 若类别数 > 调色板颜色数,会自动循环使用。 dpi : int, default=128 输出图像的分辨率(dots per inch),影响 SVG 清晰度。 返回值: -------- str : Base64 编码的 SVG 图像 Data URL,可直接用于 HTML 。 """ # === 1. 颜色准备 === # 从 Seaborn 获取指定调色板的颜色序列,长度等于数据行数 colors = chart.get_seaborn_colors(len(data_df.index), palette=color_palette) # === 2. 字体设置 === # 获取系统可用中文字体,优先使用支持中文的字体 available_font = ufont.get_fonts() plt.rcParams['font.sans-serif'] = list(available_font) plt.rcParams['axes.unicode_minus'] = False # 解决负号显示为方块的问题 # === 3. 创建画布与子图 === # 使用非白色背景,便于嵌入网页(透明背景) fig = plt.figure(figsize=(8, 6), facecolor='none', dpi=dpi) # 左侧:环形图区域 ax1 = fig.add_axes((0.1, 0.05, 0.6, 0.9)) # [left, bottom, width, height] # 右侧:图例区域(色块+文字) ax2 = fig.add_axes((0.75, 0.05, 0.2, 0.9)) # 移除两个子图的坐标轴(纯图形,无刻度) for ax in [ax1, ax2]: ax.set_axis_off() # === 4. 绘制环形图 === # 使用 wedgeprops 设置内环宽度,实现环形效果 wedges, _ = ax1.pie( data_df[value_column], # 数值列,决定扇区大小 colors=colors, # 颜色序列 startangle=90, # 从正上方开始绘制(更美观) wedgeprops=dict( width=0.6, # 环形宽度(0~1),越大越薄 edgecolor='white', # 边缘白色,提升对比度 linewidth=1 # 边缘线宽 ), radius=1.2, # 半径略大于1,避免边缘被裁剪 ) # === 5. 右侧图例:色块 + 文字 === # 图例参数定义 num_items = len(data_df.index) box_size = 0.08 # 每个色块大小(宽度和高度) text_offset = 0.05 # 文字与色块的水平间距 font_size = 24 # 字体大小 line_height = 0.1 # 每行占用高度(包括上下间距) total_legend_height = num_items * line_height start_y = 0.45 + total_legend_height / 2 # 使图例垂直居中于右侧区域 y_pos = start_y # 当前绘制的 y 坐标 # 遍历每一类,绘制色块和文本 for i, (label, color) in enumerate(zip(data_df[legend_labels], colors)): # 绘制色块矩形 ax2.add_patch( Rectangle( (0.05, y_pos - box_size / 2), # 左下角坐标:x=0.05,y居中 box_size, box_size, # 宽高均为 box_size facecolor=color, # 填充颜色 edgecolor='white', # 白色描边 lw=1 # 线宽 ) ) # 绘制文字标签:名称 + 数值 + 百分比 ax2.text( 0.05 + box_size + text_offset, # 文字起始 x 位置:色块右侧 + 间距 y_pos, # y 位置居中于色块 f"{label}({data_df[value_column][i]}台,{data_df[percentage_column][i]}%)", va='center', # 垂直居中 ha='left', # 水平左对齐 fontsize=font_size, fontweight='bold' ) y_pos -= line_height # 下移一行,准备下一个图例项 # === 6. 输出图像 === # 使用 BytesIO 缓存 SVG 图像 buffer = BytesIO() plt.savefig(buffer, format='svg', dpi=dpi, bbox_inches='tight', facecolor='none') plt.close() # 关闭图形以释放内存 # 编码为 Base64 并构造 Data URL image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') img_base64 = f"data:image/svg+xml;base64,{image_base64}" return img_base64