fix(ppt): line chart resilience — sparse data + dense xtick + inset legend
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
實戰在 188 prod 抓到的問題:N=1 時 title/legend/data marker 全擠在頂部。
修補:
- 加 `sparse = n_pts <= 2` 旗標,N<=2 時:
* 略過平均水平線(單點下無意義且標籤撞 title)
* marker 加大(5 → 7)讓單點更顯眼
* 中央加「⚠ 期間僅 N 個資料點」提示框(圓角米底+焦糖橘邊)
* legend 改 lower right inset(避開 title)
* x 軸 set_xlim 加邊距避免 marker 貼牆
- 正常 N>=3:
* legend 從 bbox_to_anchor 上方移到 axes 內 upper right
(frameon=True 米底邊框,避免與 title 撞)
* xticklabel 在 N>14 時 rotation=45 ha=right(避免 30 點擠成一團)
* title pad 加大為 18
bump 版本:
- daily v3.0.1 → v3.0.2
- weekly v3.0.1 → v3.0.2
- monthly v3.1.1 → v3.1.2
本機驗證:
- N=1:「資料不足」提示框正確顯示,無重疊
- N=30:30 個 xticklabel 旋轉 45° 清楚分離,legend 右上 inset 不撞 title
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,9 +41,9 @@ REPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
# 路由層會把版本號併入快取 key,舊快取自然 miss → 重新生成。
|
||||
# Bump 規則:major 設計改版 +0.1;微調文案不需 bump。
|
||||
TEMPLATE_VERSIONS = {
|
||||
'daily': 'v3.0.1', # 2026-05-02 (CJK fix) matplotlib fallback 涵蓋容器 Noto CJK JP
|
||||
'weekly': 'v3.0.1', # 2026-05-02 (CJK fix) 同上
|
||||
'monthly': 'v3.1.1', # 2026-05-02 (CJK fix) v3.1 + 容器 CJK 字型 fallback 修正
|
||||
'daily': 'v3.0.2', # 2026-05-02 折線稀疏資料防呆:N<=2 時 inset legend、加「資料不足」提示
|
||||
'weekly': 'v3.0.2', # 2026-05-02 同上
|
||||
'monthly': 'v3.1.2', # 2026-05-02 同上(雖月報通常 30 點不會踩到,仍 bump 統一)
|
||||
'strategy': 'v3.0', # 2026-05-02 AI 頁去黑改暖紙 + 附錄頁
|
||||
'competitor': 'v3.0', # 2026-05-02 AI 頁去黑改暖紙 + 附錄頁
|
||||
'promo': 'v3.0', # 2026-05-02 AI 頁去黑改暖紙 + 附錄頁
|
||||
@@ -619,7 +619,7 @@ def _mpl_line_chart_png(curr_dates, curr_vals, prev_vals=None,
|
||||
total_width_cm=30.0, total_height_cm=11.0,
|
||||
title="", curr_label="本月", prev_label="上月對比") -> "io.BytesIO":
|
||||
"""日業績折線:本月實線(焦糖橘)+ 上月虛線(蜂蜜金)+ 平均水平線。
|
||||
高/低點自動標註,圖例置上。
|
||||
高/低點自動標註,圖例置上(N>=3 時 ncol=3 上方;N<3 時 inset best)。
|
||||
"""
|
||||
import io
|
||||
matplotlib, plt = _mpl_setup()
|
||||
@@ -628,6 +628,9 @@ def _mpl_line_chart_png(curr_dates, curr_vals, prev_vals=None,
|
||||
|
||||
fig = None
|
||||
try:
|
||||
n_pts = len(curr_vals)
|
||||
sparse = n_pts <= 2
|
||||
|
||||
curr_wan = [float(v) / 10000.0 for v in curr_vals]
|
||||
fig_w = total_width_cm / 2.54
|
||||
fig_h = total_height_cm / 2.54
|
||||
@@ -637,7 +640,7 @@ def _mpl_line_chart_png(curr_dates, curr_vals, prev_vals=None,
|
||||
|
||||
x = list(range(len(curr_dates)))
|
||||
line_curr, = ax.plot(x, curr_wan, color="#C96442", linewidth=2.4,
|
||||
marker="o", markersize=5,
|
||||
marker="o", markersize=7 if sparse else 5,
|
||||
markerfacecolor="#C96442", markeredgecolor="#FAF7F0",
|
||||
label=curr_label, zorder=3)
|
||||
ax.fill_between(x, curr_wan, alpha=0.12, color="#C96442", zorder=1)
|
||||
@@ -651,8 +654,10 @@ def _mpl_line_chart_png(curr_dates, curr_vals, prev_vals=None,
|
||||
label=prev_label, zorder=2, alpha=0.85)
|
||||
|
||||
avg_v = sum(curr_wan) / len(curr_wan) if curr_wan else 0
|
||||
ax.axhline(avg_v, color="#8F4530", linewidth=1.0, linestyle=":",
|
||||
alpha=0.7, label=f"本月日均 {avg_v:.1f}萬")
|
||||
# 平均線僅在 N>=3 時有意義;N<=2 略過避免標籤撞 title
|
||||
if not sparse:
|
||||
ax.axhline(avg_v, color="#8F4530", linewidth=1.0, linestyle=":",
|
||||
alpha=0.7, label=f"本月日均 {avg_v:.1f}萬")
|
||||
|
||||
if curr_wan:
|
||||
max_i = curr_wan.index(max(curr_wan))
|
||||
@@ -668,7 +673,12 @@ def _mpl_line_chart_png(curr_dates, curr_vals, prev_vals=None,
|
||||
ha="center", fontsize=9, color="#B5342F", fontweight="bold")
|
||||
|
||||
ax.set_xticks(x)
|
||||
ax.set_xticklabels([str(d)[-5:] for d in curr_dates], rotation=0, fontsize=9)
|
||||
# 資料點多時旋轉 label 避免擠成一團;少時水平
|
||||
x_rotation = 45 if len(x) > 14 else 0
|
||||
ax.set_xticklabels([str(d)[-5:] for d in curr_dates],
|
||||
rotation=x_rotation,
|
||||
ha=("right" if x_rotation else "center"),
|
||||
fontsize=9)
|
||||
ax.spines["top"].set_visible(False)
|
||||
ax.spines["right"].set_visible(False)
|
||||
ax.spines["left"].set_color("#9B9081")
|
||||
@@ -679,12 +689,32 @@ def _mpl_line_chart_png(curr_dates, curr_vals, prev_vals=None,
|
||||
ax.set_axisbelow(True)
|
||||
ax.set_ylabel("日業績(萬元)", fontsize=10, color="#645C52", labelpad=8)
|
||||
|
||||
if title:
|
||||
ax.set_title(title, fontsize=13, color="#2A2520", loc="left",
|
||||
pad=12, fontweight="bold")
|
||||
# 資料稀疏時:x 軸留邊(避免 marker 貼牆)+ 加「資料不足」提示
|
||||
if sparse:
|
||||
ax.set_xlim(-0.5, max(0.5, len(x) - 0.5))
|
||||
ax.text(0.5, 0.5,
|
||||
f"⚠ 期間僅 {n_pts} 個資料點\n建議擴大查詢範圍以建立趨勢線",
|
||||
transform=ax.transAxes,
|
||||
ha="center", va="center",
|
||||
fontsize=12, color="#8F4530", alpha=0.55,
|
||||
fontweight="bold",
|
||||
bbox=dict(boxstyle="round,pad=0.6",
|
||||
facecolor="#FAF7F0",
|
||||
edgecolor="#C96442",
|
||||
linewidth=1.0, alpha=0.85))
|
||||
|
||||
ax.legend(loc="upper left", frameon=False, fontsize=10, ncol=3,
|
||||
bbox_to_anchor=(0, 1.10))
|
||||
if title:
|
||||
# title pad 加大為 18(原 12),與 legend 留更多空間
|
||||
ax.set_title(title, fontsize=13, color="#2A2520", loc="left",
|
||||
pad=18, fontweight="bold")
|
||||
|
||||
# legend 一律 inset axes 內(避免與 title 撞)
|
||||
if sparse:
|
||||
ax.legend(loc="lower right", frameon=False, fontsize=9)
|
||||
else:
|
||||
ax.legend(loc="upper right", frameon=True, fontsize=9,
|
||||
ncol=1, framealpha=0.85,
|
||||
facecolor="#FAF7F0", edgecolor="#C4BAA8")
|
||||
|
||||
plt.tight_layout()
|
||||
buf = io.BytesIO()
|
||||
|
||||
Reference in New Issue
Block a user