# -*- coding: utf-8 -*- """投标技术方案配图工具:与既有模块蓝图风格一致的扁平框图绘制。 坐标系统一为 0..100(x)× 0..100(y),左上角为 (0,100)。 """ import os import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib.patches import FancyArrowPatch, FancyBboxPatch plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei"] plt.rcParams["axes.unicode_minus"] = False ASSETS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_bid_assets") os.makedirs(ASSETS, exist_ok=True) # 与既有蓝图一致的配色 C = { "blue": "#2E75B6", "blue_d": "#1F4E79", "orange": "#E36C0A", "green": "#2E8B57", "green_d": "#1F6B3F", "purple": "#7030A0", "red": "#C00000", "teal": "#0F8B8D", "gray": "#8C8C8C", "gold": "#BF8F00", } PANEL = { "blue": "#EAF1F8", "orange": "#FDF1E4", "green": "#EAF5EE", "purple": "#F2EAF8", "gray": "#F2F2F2", "teal": "#E6F4F4", "red": "#FBEAEA", } TITLE_C = "#1F4E79" EDGE_C = "#BFBFBF" ARROW_C = "#7F7F7F" class Fig: def __init__(self, title, w=13.0, h=7.2, sub=None): self.fig, self.ax = plt.subplots(figsize=(w, h), dpi=200) self.ax.set_xlim(0, 100) self.ax.set_ylim(0, 100) self.ax.axis("off") self.fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01) top = 96.5 self.ax.text(50, top, title, ha="center", va="top", fontsize=15, fontweight="bold", color=TITLE_C) if sub: self.ax.text(50, top - 4.2, sub, ha="center", va="top", fontsize=9, color="#595959") # ---------------------------------------------------------------- 图元 def panel(self, x, y, w, h, label=None, tone="blue"): """分组底板(x,y 为左上角)。""" self.ax.add_patch(FancyBboxPatch( (x, y - h), w, h, boxstyle="round,pad=0,rounding_size=1.2", linewidth=0.8, edgecolor=EDGE_C, facecolor=PANEL[tone], zorder=1)) if label: self.ax.text(x + 1.6, y - 2.1, label, ha="left", va="center", fontsize=10, fontweight="bold", color="#404040", zorder=3) def box(self, x, y, w, h, text, tone="blue", fs=9.2, fc=None, tc="white", bold=True, edge=None): """实心圆角框(x,y 为左上角),返回锚点字典。""" face = fc or C[tone] self.ax.add_patch(FancyBboxPatch( (x, y - h), w, h, boxstyle="round,pad=0,rounding_size=0.9", linewidth=0.9 if edge else 0, edgecolor=edge or "none", facecolor=face, zorder=4)) self.ax.text(x + w / 2, y - h / 2, text, ha="center", va="center", fontsize=fs, color=tc, zorder=5, fontweight="bold" if bold else "normal", linespacing=1.45) return {"l": (x, y - h / 2), "r": (x + w, y - h / 2), "t": (x + w / 2, y), "b": (x + w / 2, y - h), "c": (x + w / 2, y - h / 2), "box": (x, y, w, h)} def obox(self, x, y, w, h, text, tone="gray", fs=9.0): """描边框(浅底深字)。""" return self.box(x, y, w, h, text, fc="white", tc="#404040", edge=C[tone], fs=fs, bold=False) def arrow(self, a, b, style="-|>", color=None, lw=1.2, rad=0.0, dashed=False): self.ax.add_patch(FancyArrowPatch( a, b, arrowstyle=style, mutation_scale=11, linewidth=lw, color=color or ARROW_C, zorder=3, linestyle="--" if dashed else "-", connectionstyle=f"arc3,rad={rad}", shrinkA=1.5, shrinkB=1.5)) def chain(self, anchors, port=("r", "l"), **kw): for a, b in zip(anchors, anchors[1:]): self.arrow(a[port[0]], b[port[1]], **kw) def note(self, x, y, text, fs=8.2, color="#595959", ha="left", va="center"): self.ax.text(x, y, text, fontsize=fs, color=color, ha=ha, va=va, linespacing=1.5, zorder=6) def elbow(self, a, b, mid=None, color=None, lw=1.3, dashed=False): """直角折线连接:a 向下出、经水平段、再进入 b 顶部。""" my = mid if mid is not None else (a[1] + b[1]) / 2 pts = [a, (a[0], my), (b[0], my)] xs = [p[0] for p in pts] ys = [p[1] for p in pts] self.ax.plot(xs, ys, color=color or ARROW_C, linewidth=lw, zorder=3, solid_capstyle="round", linestyle="--" if dashed else "-") self.arrow((b[0], my), b, color=color, lw=lw, dashed=dashed) def crop(self, ymin, ymax=100.0): self.ax.set_ylim(ymin, ymax) def save(self, name): path = os.path.join(ASSETS, name + ".png") self.fig.savefig(path, facecolor="white", bbox_inches="tight", pad_inches=0.12) plt.close(self.fig) kb = os.path.getsize(path) // 1024 print(f" [图] {name + '.png':<28} {kb:>5} KB") return path def row(x0, y, w, h, n, gap, texts, f, tone="blue", **kw): """水平等距排列 n 个框。""" out = [] for i in range(n): out.append(f.box(x0 + i * (w + gap), y, w, h, texts[i], tone=tone, **kw)) return out