| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- # -*- 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
|