Files
goodbuddy/resources/skills/longdoc-docx/diagram_kit.py
T

170 lines
6.0 KiB
Python

"""matplotlib 架构图/流程图通用工具箱(中文可用)。
不用画图工具手绘,用脚本画方框和箭头:改文字就是改字符串;配色字体统一由
常量控制;图表能进 git diff,方便 review 措辞变更。
用法:在你自己的 gen_diagrams.py 里
import sys, os
sys.path.insert(0, "<skill_dir>")
from diagram_kit import box, arrow, new_fig, save, row_layout, NAVY, RED
def diagram_architecture():
fig, ax = new_fig(13, 9.2)
box(ax, 0.5, 8.0, 12, 0.8, "接入层")
...
save(fig, "diagram1-总体技术架构图.png", out_dir=OUT_DIR)
自检字体:
python3 diagram_kit.py --check-font
"""
import os
import sys
import matplotlib
matplotlib.use("Agg") # 无显示环境必须
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
# ---------------------------------------------------------------------------
# 中文字体:matplotlib 默认字体不含中文字形,必须显式指定字体文件
# 按优先级探测;找不到时报错并给出安装提示,而不是静默输出方块字
# ---------------------------------------------------------------------------
FONT_CANDIDATES = [
("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc"),
("/usr/share/fonts/opentype/noto/NotoSansCJK-VF.otf.ttc",
"/usr/share/fonts/opentype/noto/NotoSansCJK-VF.otf.ttc"),
("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"),
("/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/PingFang.ttc"),
("C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/msyhbd.ttc"),
]
FONT_HINT = (
"未找到中文字体,图中中文会渲染成方块。安装:\n"
" Debian/Ubuntu: apt-get install fonts-noto-cjk\n"
" RHEL/CentOS: yum install google-noto-sans-cjk-ttc-fonts\n"
"确认:fc-list | grep -i 'noto sans cjk'\n"
"也可设环境变量 CJK_FONT_REGULAR / CJK_FONT_BOLD 指向字体文件。"
)
def _resolve_fonts():
reg = os.environ.get("CJK_FONT_REGULAR")
bold = os.environ.get("CJK_FONT_BOLD", reg)
if reg and os.path.exists(reg):
return reg, (bold if bold and os.path.exists(bold) else reg)
for r, b in FONT_CANDIDATES:
if os.path.exists(r):
return r, (b if os.path.exists(b) else r)
return None, None
FONT_PATH, FONT_PATH_BOLD = _resolve_fonts()
if FONT_PATH is None:
print("WARN: " + FONT_HINT, file=sys.stderr)
zh_font = fm.FontProperties()
zh_bold = fm.FontProperties(weight="bold")
else:
zh_font = fm.FontProperties(fname=FONT_PATH)
zh_bold = fm.FontProperties(fname=FONT_PATH_BOLD)
# ---------------------------------------------------------------------------
# 配色:中文商务文档惯例(藏青主色 + 红色强调 + 灰阶)
# 换主题只改这几个常量,所有图一起变
# ---------------------------------------------------------------------------
NAVY = "#1F3864"
NAVY_LIGHT = "#DCE6F1"
RED = "#C00000"
RED_LIGHT = "#FBE4E4"
GRAY = "#595959"
GRAY_LIGHT = "#F2F2F2"
WHITE = "#FFFFFF"
TEXT = "#1a1a1a"
def box(ax, x, y, w, h, text, fc=WHITE, ec=NAVY, lw=1.4, fontsize=10.5,
font=None, textcolor=TEXT,
boxstyle="round,pad=0.02,rounding_size=0.06", zorder=2):
"""圆角方框 + 居中文字。linespacing 保证多行文字换行后不挤在一起。"""
b = FancyBboxPatch((x, y), w, h, boxstyle=boxstyle, linewidth=lw,
edgecolor=ec, facecolor=fc, zorder=zorder)
ax.add_patch(b)
ax.text(x + w / 2, y + h / 2, text, ha="center", va="center",
fontsize=fontsize, fontproperties=font or zh_font,
color=textcolor, zorder=zorder + 1, linespacing=1.4)
return b
def arrow(ax, xy_from, xy_to, color=GRAY, lw=1.6, style="-|>",
connectionstyle="arc3,rad=0.0", zorder=3):
a = FancyArrowPatch(xy_from, xy_to, arrowstyle=style, mutation_scale=14,
linewidth=lw, color=color,
connectionstyle=connectionstyle, zorder=zorder)
ax.add_patch(a)
return a
def label(ax, x, y, text, fontsize=9.5, color=GRAY, ha="center", va="center",
font=None, zorder=4):
"""箭头旁的说明文字、图内小标注。"""
return ax.text(x, y, text, ha=ha, va=va, fontsize=fontsize,
fontproperties=font or zh_font, color=color, zorder=zorder)
def new_fig(w, h, dpi=200):
"""画布坐标系直接等于英寸尺寸,摆位时按网格心算即可。dpi=200 保证放大不糊。"""
fig, ax = plt.subplots(figsize=(w, h), dpi=dpi)
ax.set_xlim(0, w)
ax.set_ylim(0, h)
ax.axis("off")
return fig, ax
def save(fig, name, out_dir="."):
os.makedirs(out_dir, exist_ok=True)
path = os.path.join(out_dir, name)
fig.savefig(path, bbox_inches="tight", facecolor="white")
plt.close(fig)
print("saved:", path)
return path
def row_layout(n, start_x, total_w, gap=0.25):
"""横向等宽切分:返回 n 个 (x, width),用于并列分支摆放。
for (x, w), g in zip(row_layout(len(groups), 0.5, 12.0), groups):
box(ax, x, y0, w, h, g)
"""
w = (total_w - gap * (n - 1)) / n
return [(start_x + i * (w + gap), w) for i in range(n)]
def col_layout(n, top_y, total_h, gap=0.2):
"""纵向等高切分:返回 n 个 (y, height),自上而下。"""
h = (total_h - gap * (n - 1)) / n
return [(top_y - h - i * (h + gap), h) for i in range(n)]
def _check_font():
if FONT_PATH is None:
print("中文字体:未找到\n" + FONT_HINT)
return 1
print(f"中文字体:{FONT_PATH}")
print(f"粗体: {FONT_PATH_BOLD}")
fig, ax = new_fig(6, 2)
box(ax, 0.3, 0.5, 5.4, 1.0, "中文字体自检 CJK Font OK 123")
out = save(fig, "font_check.png", out_dir="/tmp")
print(f"已生成 {out},打开确认中文不是方块。")
return 0
if __name__ == "__main__":
if "--check-font" in sys.argv:
sys.exit(_check_font())
print(__doc__)