_bid_fig_kit.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. # -*- coding: utf-8 -*-
  2. """投标技术方案配图工具:与既有模块蓝图风格一致的扁平框图绘制。
  3. 坐标系统一为 0..100(x)× 0..100(y),左上角为 (0,100)。
  4. """
  5. import os
  6. import matplotlib
  7. matplotlib.use("Agg")
  8. import matplotlib.pyplot as plt
  9. from matplotlib.patches import FancyArrowPatch, FancyBboxPatch
  10. plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei"]
  11. plt.rcParams["axes.unicode_minus"] = False
  12. ASSETS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_bid_assets")
  13. os.makedirs(ASSETS, exist_ok=True)
  14. # 与既有蓝图一致的配色
  15. C = {
  16. "blue": "#2E75B6",
  17. "blue_d": "#1F4E79",
  18. "orange": "#E36C0A",
  19. "green": "#2E8B57",
  20. "green_d": "#1F6B3F",
  21. "purple": "#7030A0",
  22. "red": "#C00000",
  23. "teal": "#0F8B8D",
  24. "gray": "#8C8C8C",
  25. "gold": "#BF8F00",
  26. }
  27. PANEL = {
  28. "blue": "#EAF1F8",
  29. "orange": "#FDF1E4",
  30. "green": "#EAF5EE",
  31. "purple": "#F2EAF8",
  32. "gray": "#F2F2F2",
  33. "teal": "#E6F4F4",
  34. "red": "#FBEAEA",
  35. }
  36. TITLE_C = "#1F4E79"
  37. EDGE_C = "#BFBFBF"
  38. ARROW_C = "#7F7F7F"
  39. class Fig:
  40. def __init__(self, title, w=13.0, h=7.2, sub=None):
  41. self.fig, self.ax = plt.subplots(figsize=(w, h), dpi=200)
  42. self.ax.set_xlim(0, 100)
  43. self.ax.set_ylim(0, 100)
  44. self.ax.axis("off")
  45. self.fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
  46. top = 96.5
  47. self.ax.text(50, top, title, ha="center", va="top",
  48. fontsize=15, fontweight="bold", color=TITLE_C)
  49. if sub:
  50. self.ax.text(50, top - 4.2, sub, ha="center", va="top",
  51. fontsize=9, color="#595959")
  52. # ---------------------------------------------------------------- 图元
  53. def panel(self, x, y, w, h, label=None, tone="blue"):
  54. """分组底板(x,y 为左上角)。"""
  55. self.ax.add_patch(FancyBboxPatch(
  56. (x, y - h), w, h, boxstyle="round,pad=0,rounding_size=1.2",
  57. linewidth=0.8, edgecolor=EDGE_C, facecolor=PANEL[tone], zorder=1))
  58. if label:
  59. self.ax.text(x + 1.6, y - 2.1, label, ha="left", va="center",
  60. fontsize=10, fontweight="bold", color="#404040", zorder=3)
  61. def box(self, x, y, w, h, text, tone="blue", fs=9.2, fc=None, tc="white",
  62. bold=True, edge=None):
  63. """实心圆角框(x,y 为左上角),返回锚点字典。"""
  64. face = fc or C[tone]
  65. self.ax.add_patch(FancyBboxPatch(
  66. (x, y - h), w, h, boxstyle="round,pad=0,rounding_size=0.9",
  67. linewidth=0.9 if edge else 0, edgecolor=edge or "none",
  68. facecolor=face, zorder=4))
  69. self.ax.text(x + w / 2, y - h / 2, text, ha="center", va="center",
  70. fontsize=fs, color=tc, zorder=5,
  71. fontweight="bold" if bold else "normal", linespacing=1.45)
  72. return {"l": (x, y - h / 2), "r": (x + w, y - h / 2),
  73. "t": (x + w / 2, y), "b": (x + w / 2, y - h),
  74. "c": (x + w / 2, y - h / 2), "box": (x, y, w, h)}
  75. def obox(self, x, y, w, h, text, tone="gray", fs=9.0):
  76. """描边框(浅底深字)。"""
  77. return self.box(x, y, w, h, text, fc="white", tc="#404040",
  78. edge=C[tone], fs=fs, bold=False)
  79. def arrow(self, a, b, style="-|>", color=None, lw=1.2, rad=0.0, dashed=False):
  80. self.ax.add_patch(FancyArrowPatch(
  81. a, b, arrowstyle=style, mutation_scale=11,
  82. linewidth=lw, color=color or ARROW_C, zorder=3,
  83. linestyle="--" if dashed else "-",
  84. connectionstyle=f"arc3,rad={rad}", shrinkA=1.5, shrinkB=1.5))
  85. def chain(self, anchors, port=("r", "l"), **kw):
  86. for a, b in zip(anchors, anchors[1:]):
  87. self.arrow(a[port[0]], b[port[1]], **kw)
  88. def note(self, x, y, text, fs=8.2, color="#595959", ha="left", va="center"):
  89. self.ax.text(x, y, text, fontsize=fs, color=color, ha=ha, va=va,
  90. linespacing=1.5, zorder=6)
  91. def elbow(self, a, b, mid=None, color=None, lw=1.3, dashed=False):
  92. """直角折线连接:a 向下出、经水平段、再进入 b 顶部。"""
  93. my = mid if mid is not None else (a[1] + b[1]) / 2
  94. pts = [a, (a[0], my), (b[0], my)]
  95. xs = [p[0] for p in pts]
  96. ys = [p[1] for p in pts]
  97. self.ax.plot(xs, ys, color=color or ARROW_C, linewidth=lw, zorder=3,
  98. solid_capstyle="round",
  99. linestyle="--" if dashed else "-")
  100. self.arrow((b[0], my), b, color=color, lw=lw, dashed=dashed)
  101. def crop(self, ymin, ymax=100.0):
  102. self.ax.set_ylim(ymin, ymax)
  103. def save(self, name):
  104. path = os.path.join(ASSETS, name + ".png")
  105. self.fig.savefig(path, facecolor="white", bbox_inches="tight",
  106. pad_inches=0.12)
  107. plt.close(self.fig)
  108. kb = os.path.getsize(path) // 1024
  109. print(f" [图] {name + '.png':<28} {kb:>5} KB")
  110. return path
  111. def row(x0, y, w, h, n, gap, texts, f, tone="blue", **kw):
  112. """水平等距排列 n 个框。"""
  113. out = []
  114. for i in range(n):
  115. out.append(f.box(x0 + i * (w + gap), y, w, h, texts[i], tone=tone, **kw))
  116. return out