107 lines
4.0 KiB
Python
107 lines
4.0 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Gantt-style timeline for 3D OpticsFEM (target: late July 2026)."""
|
||
from __future__ import annotations
|
||
|
||
from datetime import date, timedelta
|
||
from pathlib import Path
|
||
|
||
import matplotlib.pyplot as plt
|
||
import matplotlib.dates as mdates
|
||
import matplotlib.patches as mpatches
|
||
|
||
OUT = Path(__file__).resolve().parent
|
||
|
||
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei", "Arial"]
|
||
plt.rcParams["axes.unicode_minus"] = False
|
||
|
||
GREEN = "#75BD42"
|
||
GREEN_D = "#4A7C3F"
|
||
BLUE = "#4874CB"
|
||
ORANGE = "#EE822F"
|
||
GRAY = "#CCCCCC"
|
||
DARK = "#2C3E50"
|
||
WHITE = "#FFFFFF"
|
||
|
||
# (label, start, end, color, status)
|
||
# status: done | doing | plan | stretch
|
||
T0 = date(2026, 6, 1)
|
||
ITEMS = [
|
||
("阶段0 工程基础", date(2026, 6, 1), date(2026, 6, 15), GREEN, "done"),
|
||
("阶段2 PEC/ELE", date(2026, 6, 10), date(2026, 6, 20), GREEN, "done"),
|
||
("阶段3 PBC 单/双周期", date(2026, 6, 15), date(2026, 6, 25), GREEN, "done"),
|
||
("阶段1 本征频率 Run/Post", date(2026, 6, 23), date(2026, 7, 5), ORANGE, "doing"),
|
||
("阶段4 BELE/源项边界", date(2026, 6, 28), date(2026, 7, 12), BLUE, "plan"),
|
||
("算例验证 & 文档整理", date(2026, 7, 8), date(2026, 7, 18), BLUE, "plan"),
|
||
(".em 接口适配(基础)", date(2026, 7, 10), date(2026, 7, 25), BLUE, "plan"),
|
||
("阶段5 PML/Port(可选)", date(2026, 7, 20), date(2026, 8, 10), GRAY, "stretch"),
|
||
("阶段6 本征模式/二阶(可选)", date(2026, 8, 1), date(2026, 8, 31), GRAY, "stretch"),
|
||
]
|
||
|
||
MILESTONES = [
|
||
(date(2026, 6, 25), "阶段1–3\n验收"),
|
||
(date(2026, 7, 18), "MVP\n内测"),
|
||
(date(2026, 7, 31), "7月下旬\n交付"),
|
||
]
|
||
|
||
|
||
def main() -> None:
|
||
fig, ax = plt.subplots(figsize=(14, 6.2), facecolor=WHITE)
|
||
|
||
y = len(ITEMS)
|
||
for i, (label, start, end, color, status) in enumerate(ITEMS):
|
||
yy = y - i
|
||
width = (end - start).days + 1
|
||
hatch = "" if status != "stretch" else "///"
|
||
alpha = 1.0 if status != "stretch" else 0.55
|
||
ax.barh(
|
||
yy, width, left=mdates.date2num(start), height=0.55,
|
||
color=color, edgecolor=DARK, linewidth=0.8,
|
||
hatch=hatch, alpha=alpha,
|
||
)
|
||
mid = start + timedelta(days=width // 2)
|
||
ax.text(
|
||
mdates.date2num(mid), yy, label,
|
||
ha="center", va="center", fontsize=9.5,
|
||
color=WHITE if status in ("done", "doing") else DARK,
|
||
fontweight="bold" if status == "doing" else "normal",
|
||
)
|
||
|
||
for ms_date, ms_text in MILESTONES:
|
||
x = mdates.date2num(ms_date)
|
||
ax.axvline(x, color="#C0392B", ls="--", lw=1.2, alpha=0.75, zorder=0)
|
||
ax.text(x, y + 0.55, ms_text, ha="center", va="bottom", fontsize=8.5,
|
||
color="#C0392B", fontweight="bold")
|
||
|
||
ax.set_yticks([])
|
||
ax.set_ylim(0.3, y + 1.2)
|
||
ax.set_xlim(mdates.date2num(date(2026, 5, 28)), mdates.date2num(date(2026, 9, 5)))
|
||
ax.xaxis.set_major_locator(mdates.WeekdayLocator(byweekday=mdates.MO, interval=1))
|
||
ax.xaxis.set_major_formatter(mdates.DateFormatter("%m/%d"))
|
||
ax.xaxis.set_minor_locator(mdates.DayLocator(interval=1))
|
||
ax.grid(axis="x", alpha=0.25, ls="--")
|
||
ax.spines["top"].set_visible(False)
|
||
ax.spines["right"].set_visible(False)
|
||
ax.spines["left"].set_visible(False)
|
||
|
||
ax.set_title(
|
||
"OpticsFEM 3D 扩展 — 时间节点规划(目标:2026年7月下旬交付)",
|
||
fontsize=14, fontweight="bold", color=DARK, pad=14,
|
||
)
|
||
|
||
legend_items = [
|
||
mpatches.Patch(facecolor=GREEN, edgecolor=DARK, label="已完成"),
|
||
mpatches.Patch(facecolor=ORANGE, edgecolor=DARK, label="进行中"),
|
||
mpatches.Patch(facecolor=BLUE, edgecolor=DARK, label="7月计划"),
|
||
mpatches.Patch(facecolor=GRAY, edgecolor=DARK, hatch="///", alpha=0.55, label="8月及以后(扩展)"),
|
||
]
|
||
ax.legend(handles=legend_items, loc="lower right", fontsize=9, framealpha=0.95)
|
||
|
||
out = OUT / "timeline_july2026.png"
|
||
fig.savefig(out, dpi=180, bbox_inches="tight", facecolor=WHITE)
|
||
plt.close(fig)
|
||
print(f"Saved {out}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|