|
@@ -1,9 +1,39 @@
|
|
|
"""缺陷分析页面的可测试业务逻辑。"""
|
|
"""缺陷分析页面的可测试业务逻辑。"""
|
|
|
|
|
|
|
|
|
|
+import base64
|
|
|
import html
|
|
import html
|
|
|
|
|
+import io
|
|
|
|
|
+import os
|
|
|
|
|
+
|
|
|
|
|
+import matplotlib
|
|
|
|
|
+matplotlib.use("Agg")
|
|
|
|
|
+import matplotlib.pyplot as plt
|
|
|
|
|
+from matplotlib import font_manager as fm
|
|
|
import numpy as np
|
|
import numpy as np
|
|
|
import pandas as pd
|
|
import pandas as pd
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+def _setup_chinese_font():
|
|
|
|
|
+ """配置 matplotlib 中文字体,与 app.py 保持一致。"""
|
|
|
|
|
+ font_paths = [
|
|
|
|
|
+ r"C:\Windows\Fonts\msyh.ttc",
|
|
|
|
|
+ r"C:\Windows\Fonts\simhei.ttf",
|
|
|
|
|
+ r"C:\Windows\Fonts\simsun.ttc",
|
|
|
|
|
+ r"C:\Windows\Fonts\malgun.ttf",
|
|
|
|
|
+ ]
|
|
|
|
|
+ for fp in font_paths:
|
|
|
|
|
+ if os.path.exists(fp):
|
|
|
|
|
+ font_prop = fm.FontProperties(fname=fp)
|
|
|
|
|
+ plt.rcParams["font.family"] = font_prop.get_name()
|
|
|
|
|
+ plt.rcParams["axes.unicode_minus"] = False
|
|
|
|
|
+ return font_prop
|
|
|
|
|
+ plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "Arial Unicode MS"]
|
|
|
|
|
+ plt.rcParams["axes.unicode_minus"] = False
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+_CHINESE_FONT_PROP = _setup_chinese_font()
|
|
|
|
|
+
|
|
|
from defect_analysis.ml.model_bundle import create_model_bundle
|
|
from defect_analysis.ml.model_bundle import create_model_bundle
|
|
|
from defect_analysis.ml.predict import predict_key_factors
|
|
from defect_analysis.ml.predict import predict_key_factors
|
|
|
from defect_analysis.root_cause import EXTENDED_ROOT_CAUSE_DIMENSIONS, build_extended_root_causes
|
|
from defect_analysis.root_cause import EXTENDED_ROOT_CAUSE_DIMENSIONS, build_extended_root_causes
|
|
@@ -379,6 +409,103 @@ def build_ml_factor_insights(
|
|
|
return base
|
|
return base
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _fig_to_base64(fig, *, dpi=120):
|
|
|
|
|
+ """把 matplotlib Figure 转成 base64 PNG data URI。"""
|
|
|
|
|
+ buf = io.BytesIO()
|
|
|
|
|
+ fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight", facecolor="white")
|
|
|
|
|
+ buf.seek(0)
|
|
|
|
|
+ encoded = base64.b64encode(buf.read()).decode("utf-8")
|
|
|
|
|
+ buf.close()
|
|
|
|
|
+ plt.close(fig)
|
|
|
|
|
+ return f"data:image/png;base64,{encoded}"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def generate_report_charts(filtered_df, *, daily_trend_df=None):
|
|
|
|
|
+ """生成报告内嵌的三张核心图表,返回 dict of base64 data URIs。"""
|
|
|
|
|
+ charts = {}
|
|
|
|
|
+
|
|
|
|
|
+ # --- 1. 缺陷类型分布条形图 ---
|
|
|
|
|
+ type_counts = filtered_df["defect_type"].value_counts().head(10)
|
|
|
|
|
+ if not type_counts.empty:
|
|
|
|
|
+ fig, ax = plt.subplots(figsize=(7, 3.5))
|
|
|
|
|
+ colors = ["#0f766e", "#14b8a6", "#22d3ee", "#38bdf8", "#60a5fa",
|
|
|
|
|
+ "#a78bfa", "#c084fc", "#e879f9", "#f472b6", "#fb7185"]
|
|
|
|
|
+ bars = ax.barh(
|
|
|
|
|
+ range(len(type_counts)),
|
|
|
|
|
+ type_counts.values,
|
|
|
|
|
+ color=colors[: len(type_counts)],
|
|
|
|
|
+ )
|
|
|
|
|
+ ax.set_yticks(range(len(type_counts)))
|
|
|
|
|
+ ax.set_yticklabels(type_counts.index, fontsize=11)
|
|
|
|
|
+ ax.invert_yaxis()
|
|
|
|
|
+ for i, (bar, val) in enumerate(zip(bars, type_counts.values)):
|
|
|
|
|
+ ax.text(bar.get_width() + max(type_counts.values) * 0.01,
|
|
|
|
|
+ bar.get_y() + bar.get_height() / 2,
|
|
|
|
|
+ str(val), va="center", fontsize=10, fontweight="bold")
|
|
|
|
|
+ ax.set_title("缺陷类型 TOP 10", fontsize=13, fontweight="bold", pad=12)
|
|
|
|
|
+ ax.spines["top"].set_visible(False)
|
|
|
|
|
+ ax.spines["right"].set_visible(False)
|
|
|
|
|
+ ax.set_xlabel("缺陷数")
|
|
|
|
|
+ charts["type_distribution"] = _fig_to_base64(fig)
|
|
|
|
|
+
|
|
|
|
|
+ # --- 2. 每日趋势折线图 ---
|
|
|
|
|
+ if daily_trend_df is not None and not daily_trend_df.empty:
|
|
|
|
|
+ daily = daily_trend_df.copy()
|
|
|
|
|
+ daily["day"] = pd.to_datetime(daily["day"])
|
|
|
|
|
+ daily = daily.sort_values("day")
|
|
|
|
|
+ fig, ax = plt.subplots(figsize=(7, 3))
|
|
|
|
|
+ ax.plot(daily["day"], daily["缺陷数"], marker="o", linewidth=2,
|
|
|
|
|
+ markersize=5, color="#0f766e")
|
|
|
|
|
+ ax.fill_between(daily["day"], daily["缺陷数"], alpha=0.15, color="#0f766e")
|
|
|
|
|
+ ax.set_title("每日缺陷数趋势", fontsize=13, fontweight="bold", pad=10)
|
|
|
|
|
+ ax.spines["top"].set_visible(False)
|
|
|
|
|
+ ax.spines["right"].set_visible(False)
|
|
|
|
|
+ ax.tick_params(axis="x", rotation=30)
|
|
|
|
|
+ charts["daily_trend"] = _fig_to_base64(fig)
|
|
|
|
|
+
|
|
|
|
|
+ # --- 3. 设备缺陷分布 ---
|
|
|
|
|
+ eq_counts = filtered_df.get("equipment_id")
|
|
|
|
|
+ if eq_counts is not None:
|
|
|
|
|
+ eq_counts = eq_counts.value_counts().head(8)
|
|
|
|
|
+ if not eq_counts.empty:
|
|
|
|
|
+ fig, ax = plt.subplots(figsize=(7, 3))
|
|
|
|
|
+ ax.bar(
|
|
|
|
|
+ range(len(eq_counts)),
|
|
|
|
|
+ eq_counts.values,
|
|
|
|
|
+ color=["#1e3a5f", "#2563eb", "#3b82f6", "#60a5fa",
|
|
|
|
|
+ "#93c5fd", "#0d9488", "#14b8a6", "#2dd4bf"][: len(eq_counts)],
|
|
|
|
|
+ )
|
|
|
|
|
+ ax.set_xticks(range(len(eq_counts)))
|
|
|
|
|
+ ax.set_xticklabels(eq_counts.index, rotation=25, ha="right", fontsize=10)
|
|
|
|
|
+ ax.set_title("设备缺陷分布 TOP 8", fontsize=13, fontweight="bold", pad=10)
|
|
|
|
|
+ ax.spines["top"].set_visible(False)
|
|
|
|
|
+ ax.spines["right"].set_visible(False)
|
|
|
|
|
+ ax.set_ylabel("缺陷数")
|
|
|
|
|
+ for i, val in enumerate(eq_counts.values):
|
|
|
|
|
+ ax.text(i, val + max(eq_counts.values) * 0.02, str(val),
|
|
|
|
|
+ ha="center", fontsize=9, fontweight="bold")
|
|
|
|
|
+ charts["equipment_distribution"] = _fig_to_base64(fig)
|
|
|
|
|
+
|
|
|
|
|
+ # --- 4. 严重程度饼图 ---
|
|
|
|
|
+ if "severity" in filtered_df.columns and not filtered_df.empty:
|
|
|
|
|
+ sev_counts = filtered_df["severity"].value_counts()
|
|
|
|
|
+ if not sev_counts.empty:
|
|
|
|
|
+ fig, ax = plt.subplots(figsize=(4.5, 3.5))
|
|
|
|
|
+ sev_colors = {"轻微": "#22c55e", "一般": "#f59e0b", "严重": "#ef4444"}
|
|
|
|
|
+ colors = [sev_colors.get(name, "#94a3b8") for name in sev_counts.index]
|
|
|
|
|
+ wedges, texts, autotexts = ax.pie(
|
|
|
|
|
+ sev_counts.values, labels=sev_counts.index,
|
|
|
|
|
+ autopct="%1.1f%%", colors=colors, startangle=90,
|
|
|
|
|
+ textprops={"fontsize": 11},
|
|
|
|
|
+ )
|
|
|
|
|
+ for at in autotexts:
|
|
|
|
|
+ at.set_fontweight("bold")
|
|
|
|
|
+ ax.set_title("严重程度占比", fontsize=13, fontweight="bold", pad=10)
|
|
|
|
|
+ charts["severity_pie"] = _fig_to_base64(fig)
|
|
|
|
|
+
|
|
|
|
|
+ return charts
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _escape(value):
|
|
def _escape(value):
|
|
|
return html.escape(str(value), quote=True)
|
|
return html.escape(str(value), quote=True)
|
|
|
|
|
|
|
@@ -403,10 +530,12 @@ def build_html_report(
|
|
|
trend_summary="-",
|
|
trend_summary="-",
|
|
|
anomaly_rows=None,
|
|
anomaly_rows=None,
|
|
|
recommendations=None,
|
|
recommendations=None,
|
|
|
|
|
+ charts=None,
|
|
|
):
|
|
):
|
|
|
"""生成可直接在浏览器打开的自包含综合 HTML 报告。"""
|
|
"""生成可直接在浏览器打开的自包含综合 HTML 报告。"""
|
|
|
anomaly_rows = anomaly_rows or []
|
|
anomaly_rows = anomaly_rows or []
|
|
|
recommendations = recommendations or []
|
|
recommendations = recommendations or []
|
|
|
|
|
+ charts = charts or {}
|
|
|
type_rows = _series_rows(type_counts)
|
|
type_rows = _series_rows(type_counts)
|
|
|
equipment_rows = _series_rows(equipment_counts)
|
|
equipment_rows = _series_rows(equipment_counts)
|
|
|
seat_rows = _series_rows(seat_top)
|
|
seat_rows = _series_rows(seat_top)
|
|
@@ -492,6 +621,7 @@ def build_html_report(
|
|
|
.note {{ color: var(--muted); font-size: 13px; margin-top: 8px; }}
|
|
.note {{ color: var(--muted); font-size: 13px; margin-top: 8px; }}
|
|
|
.recommend {{ border-left: 5px solid var(--brand); }}
|
|
.recommend {{ border-left: 5px solid var(--brand); }}
|
|
|
li {{ margin: 8px 0; }}
|
|
li {{ margin: 8px 0; }}
|
|
|
|
|
+ .chart {{ max-width: 100%; border-radius: 12px; margin-top: 8px; }}
|
|
|
@media print {{
|
|
@media print {{
|
|
|
body {{ background: white; }}
|
|
body {{ background: white; }}
|
|
|
.page {{ max-width: none; padding: 20px; }}
|
|
.page {{ max-width: none; padding: 20px; }}
|
|
@@ -529,18 +659,21 @@ def build_html_report(
|
|
|
<div class="card">
|
|
<div class="card">
|
|
|
<h2>2. 趋势分析</h2>
|
|
<h2>2. 趋势分析</h2>
|
|
|
<p>{_escape(trend_summary)}</p>
|
|
<p>{_escape(trend_summary)}</p>
|
|
|
|
|
+ {('<img class="chart" src="' + charts["daily_trend"] + '" alt="每日缺陷数趋势"/>') if "daily_trend" in charts else ""}
|
|
|
<p class="note">建议结合 SPC 控制图确认是否越过预警线或控制线。</p>
|
|
<p class="note">建议结合 SPC 控制图确认是否越过预警线或控制线。</p>
|
|
|
</div>
|
|
</div>
|
|
|
</section>
|
|
</section>
|
|
|
|
|
|
|
|
<section class="card">
|
|
<section class="card">
|
|
|
<h2>3. 缺陷类型分布</h2>
|
|
<h2>3. 缺陷类型分布</h2>
|
|
|
|
|
+ {('<img class="chart" src="' + charts["type_distribution"] + '" alt="缺陷类型分布"/>') if "type_distribution" in charts else ""}
|
|
|
<table><tr><th>缺陷类型</th><th>缺陷数</th><th>占比</th></tr>{type_items}</table>
|
|
<table><tr><th>缺陷类型</th><th>缺陷数</th><th>占比</th></tr>{type_items}</table>
|
|
|
</section>
|
|
</section>
|
|
|
|
|
|
|
|
<section class="two">
|
|
<section class="two">
|
|
|
<div class="card">
|
|
<div class="card">
|
|
|
<h2>4. 设备分布</h2>
|
|
<h2>4. 设备分布</h2>
|
|
|
|
|
+ {('<img class="chart" src="' + charts["equipment_distribution"] + '" alt="设备缺陷分布"/>') if "equipment_distribution" in charts else ""}
|
|
|
<table><tr><th>设备</th><th>缺陷数</th></tr>{equipment_items}</table>
|
|
<table><tr><th>设备</th><th>缺陷数</th></tr>{equipment_items}</table>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="card">
|
|
<div class="card">
|
|
@@ -549,13 +682,15 @@ def build_html_report(
|
|
|
</div>
|
|
</div>
|
|
|
</section>
|
|
</section>
|
|
|
|
|
|
|
|
|
|
+ {('<section class="card"><h2>6. 严重程度占比</h2><div style="display:flex;justify-content:center;"><img class="chart" src="' + charts["severity_pie"] + '" alt="严重程度占比" style="max-width:320px;"/></div></section>') if "severity_pie" in charts else ""}
|
|
|
|
|
+
|
|
|
<section class="card">
|
|
<section class="card">
|
|
|
- <h2>6. 异常检测</h2>
|
|
|
|
|
|
|
+ <h2>{"6. 异常检测" if "severity_pie" not in charts else "7. 异常检测"}</h2>
|
|
|
<table><tr><th>设备</th><th>座号</th><th>缺陷数</th></tr>{anomaly_items}</table>
|
|
<table><tr><th>设备</th><th>座号</th><th>缺陷数</th></tr>{anomaly_items}</table>
|
|
|
</section>
|
|
</section>
|
|
|
|
|
|
|
|
<section class="card recommend">
|
|
<section class="card recommend">
|
|
|
- <h2>7. 排查建议</h2>
|
|
|
|
|
|
|
+ <h2>{"7. 排查建议" if "severity_pie" not in charts else "8. 排查建议"}</h2>
|
|
|
<ul>{recommendation_items}</ul>
|
|
<ul>{recommendation_items}</ul>
|
|
|
</section>
|
|
</section>
|
|
|
|
|
|