更新 汇总图和单独图的 Cell排序和SN排序

This commit is contained in:
2026-03-27 14:32:56 +08:00
parent b1e70bee86
commit 4e4d132066

View File

@@ -316,13 +316,46 @@ HTML_TEMPLATE = """
<div class="sn-plots-container">
{% for sn_plot in test.sn_plot_images %}
<div class="sn-plot-item">
<div class="sn-plot-title">SN: {{ sn_plot.sn }}</div>
<div class="sn-plot-title">
{% if sn_plot.cell_no %}
Cell: {{ sn_plot.cell_no }}, SN: {{ sn_plot.sn_no }}
{% else %}
SN: {{ sn_plot.sn }}
{% endif %}
</div>
{% if sn_plot.has_cell_data %}
<div class="cell-info">
<small>Cell编号信息:</small>
<table style="width:100%; font-size:12px; margin:10px 0; border-collapse: collapse;">
<tr style="background-color:#f0f0f0;">
<th style="padding:5px; border:1px solid #ddd;">Cell编号</th>
<th style="padding:5px; border:1px solid #ddd;">数据点</th>
<th style="padding:5px; border:1px solid #ddd;">均值</th>
<th style="padding:5px; border:1px solid #ddd;">标准差</th>
<th style="padding:5px; border:1px solid #ddd;">最小值</th>
<th style="padding:5px; border:1px solid #ddd;">最大值</th>
</tr>
{% for cell_no, stats_dict in sn_plot.cell_info.items() %}
<tr>
<td style="padding:5px; border:1px solid #ddd;">{{ cell_no }}</td>
<td style="padding:5px; border:1px solid #ddd;">{{ stats_dict.count }}</td>
<td style="padding:5px; border:1px solid #ddd;">{{ "%.4f"|format(stats_dict.mean) }}</td>
<td style="padding:5px; border:1px solid #ddd;">{{ "%.4f"|format(stats_dict.std) }}</td>
<td style="padding:5px; border:1px solid #ddd;">{{ "%.4f"|format(stats_dict.min) }}</td>
<td style="padding:5px; border:1px solid #ddd;">{{ "%.4f"|format(stats_dict.max) }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
<img src="data:image/png;base64,{{ sn_plot.image }}" alt="{{ test.name }} - SN {{ sn_plot.sn }} 散点图" class="plot-image">
</div>
{% endfor %}
</div>
{% endif %}
{% if not loop.last %}
<hr style="margin: 20px 0; border: none; border-top: 1px solid #e0e0e0;">
{% endif %}
@@ -398,7 +431,17 @@ class MultiFileTestReportScatterPlotter:
self.folder_path: Optional[str] = None
self.df: Optional[pd.DataFrame] = None
self.output_dir: Optional[str] = None
self.required_columns = ["Test Name New", "SN", "Measurement", "Test Time", "Lower Limit", "Upper Limit"]
# self.required_columns = ["Test Name New", "SN", "Measurement", "Test Time", "Lower Limit", "Upper Limit"]
# 更新required_columns增加Cell编号
self.required_columns = [
"Test Name New",
"SN",
"Measurement",
"Test Time",
"Lower Limit",
"Upper Limit",
"Cell" # 新增Cell编号列
]
self.col_lower: Optional[str] = None
self.col_upper: Optional[str] = None
self.html_report_path: Optional[str] = None
@@ -919,6 +962,26 @@ class MultiFileTestReportScatterPlotter:
test_data['TestTime_dt'] = self._clean_and_convert_series(
test_data['Test Time'], 'datetime'
)
# 确保Cell编号存在如果原数据中有
if 'Cell' in test_data.columns:
# 清理数据:去除首尾空格,并尝试转换为数值类型
test_data['Cell编号'] = test_data['Cell'].astype(str).str.strip()
# 尝试将清理后的字符串转换为数值(例如整数)
# errors='coerce' 会将无法转换的值设为NaN非数字
test_data['Cell编号_数值'] = pd.to_numeric(test_data['Cell编号'], errors='coerce')
# 检查是否存在转换失败的值即NaN
failed_conversions = test_data['Cell编号_数值'].isna().sum()
if failed_conversions > 0:
print(f"警告:发现 {failed_conversions}'Cell' 值无法转换为数字,这些条目将保留为字符串或根据业务逻辑处理。")
# 业务决策:对于无法转换的,可以保留原字符串,或使用一个默认值
# 例如,将无法转换的条目其数值编号设为-1或一个特定的标识值
# test_data.loc[test_data['Cell编号_数值'].isna(), 'Cell编号_数值'] = -1
# 此时,您可以根据需求选择使用 'Cell编号'(字符串)或 'Cell编号_数值'(数字)进行后续分组和可视化
# 对于绘图着色和排序,使用 'Cell编号_数值' 列
grouping_column = 'Cell编号_数值'
# 去除无效数据
valid_data = test_data.dropna(subset=['Measurement_num', 'TestTime_dt'])
@@ -952,11 +1015,37 @@ class MultiFileTestReportScatterPlotter:
"""创建汇总图所有SN在一个图中"""
fig, ax = plt.subplots(figsize=(12, 8))
# 检查是否有Cell编号列
has_cell_no = 'Cell编号' in test_data.columns
# 分组绘制
groups = list(test_data.groupby("SN")) if "SN" in test_data.columns else [("Unknown_SN", test_data)]
for sn, group in groups:
if has_cell_no and not test_data['Cell编号'].isna().all():
# 先按Cell编号_数值排序再按SN排序
test_data_sorted = test_data.sort_values(['Cell编号_数值', 'SN'])
# 按Cell编号_数值和SN分组
cell_sn_groups = list(test_data_sorted.groupby(['Cell编号_数值', 'SN']))
# 生成颜色映射
colors = plt.cm.Set3(np.linspace(0, 1, len(cell_sn_groups)))
for idx, ((cell_no, sn), group) in enumerate(cell_sn_groups):
label = f"Cell:{cell_no}, SN:{sn}"
ax.scatter(group['TestTime_dt'], group['Measurement_num'],
label=str(sn), alpha=0.7, s=25)
color=colors[idx], alpha=0.7, s=25, label=label)
else:
# 只按SN分组排序
test_data_sorted = test_data.sort_values('SN')
sn_groups = [(f"{sn}_no_cell", group) for sn, group in test_data_sorted.groupby("SN")]
# 生成颜色映射
colors = plt.cm.Set3(np.linspace(0, 1, len(sn_groups)))
for idx, (group_key, group) in enumerate(sn_groups):
sn = str(group_key).replace('_no_cell', '')
label = f"SN: {sn}"
ax.scatter(group['TestTime_dt'], group['Measurement_num'],
color=colors[idx], alpha=0.7, s=25, label=label)
# 计算统计信息
y_data = test_data['Measurement_num']
@@ -977,7 +1066,10 @@ class MultiFileTestReportScatterPlotter:
linestyles='-.', linewidth=1.5, alpha=0.7, label='Median')
# 设置图形属性
ax.set_title(f"汇总图 - {test_name}")
title = f"汇总图 - {test_name}"
if has_cell_no and not test_data['Cell编号'].isna().all():
title += " (按Cell→SN排序)"
ax.set_title(title)
ax.set_xlabel("Test Time")
ax.set_ylabel("Measurement Value")
ax.grid(True, alpha=0.3)
@@ -994,7 +1086,26 @@ class MultiFileTestReportScatterPlotter:
if "SN" not in test_data.columns:
return sn_plots
sn_groups = test_data.groupby("SN")
# 检查是否有Cell编号列
has_cell_no = 'Cell编号' in test_data.columns
# 如果有Cell编号先按Cell编号_数值排序再按SN排序
if has_cell_no and not test_data['Cell编号'].isna().all():
# 先按Cell编号_数值排序再按SN排序
test_data_sorted = test_data.sort_values(['Cell编号_数值', 'SN'])
# 按Cell编号_数值分组然后对每个Cell内的数据按SN排序
cell_groups = test_data_sorted.groupby('Cell编号_数值')
for cell_no, cell_group in cell_groups:
if cell_group.empty:
continue
# 对当前Cell内的数据按SN排序
cell_group_sorted = cell_group.sort_values('SN')
# 按SN分组
sn_groups = cell_group_sorted.groupby('SN')
for sn, group in sn_groups:
if group.empty:
@@ -1002,7 +1113,72 @@ class MultiFileTestReportScatterPlotter:
fig, ax = plt.subplots(figsize=(10, 6))
# 绘制当前SN的数据点
# 绘制当前SN和Cell的数据点
ax.scatter(group['TestTime_dt'], group['Measurement_num'],
color='blue', alpha=0.7, s=30, label=f"SN:{sn}, Cell:{cell_no}")
# 计算当前SN和Cell组合的统计信息
y_data = group['Measurement_num']
stats = self._calculate_statistics(y_data)
# 绘制限值线
x_min, x_max = group['TestTime_dt'].min(), group['TestTime_dt'].max()
if lower_plot is not None:
ax.axhline(y=lower_plot, color='green', linestyle='--', linewidth=1.2, label="Lower Limit")
if upper_plot is not None:
ax.axhline(y=upper_plot, color='red', linestyle='--', linewidth=1.2, label="Upper Limit")
# 添加统计线
ax.hlines(y=stats['mean'], xmin=x_min, xmax=x_max, colors='orange',
linestyles='-', linewidth=1.5, alpha=0.7, label='Mean')
ax.hlines(y=stats['median'], xmin=x_min, xmax=x_max, colors='purple',
linestyles='-.', linewidth=1.5, alpha=0.7, label='Median')
# 设置图形属性
ax.set_title(f"SN独立图 - {test_name} (Cell: {cell_no}, SN: {sn})")
ax.set_xlabel("Test Time")
ax.set_ylabel("Measurement Value")
ax.grid(True, alpha=0.3)
ax.tick_params(axis='x', rotation=45)
ax.legend()
# 转换为base64
plot_image = self._plot_to_base64(fig)
# 收集当前Cell编号的统计信息 - 修复格式
cell_info = {}
if has_cell_no:
# 计算当前Cell编号的详细统计信息
cell_stats_dict = {
'count': len(group),
'mean': float(group['Measurement_num'].mean()),
'std': float(group['Measurement_num'].std()),
'min': float(group['Measurement_num'].min()),
'max': float(group['Measurement_num'].max())
}
cell_info = {str(cell_no): cell_stats_dict}
sn_plots.append({
"sn": f"Cell_{cell_no}_SN_{sn}",
"image": plot_image,
"cell_info": cell_info if cell_info else None,
"has_cell_data": True,
"cell_no": str(cell_no),
"sn_no": str(sn)
})
else:
# 没有Cell编号只按SN分组排序
test_data_sorted = test_data.sort_values('SN')
sn_groups = test_data_sorted.groupby("SN")
for sn, group in sn_groups:
if group.empty:
continue
fig, ax = plt.subplots(figsize=(10, 6))
# 没有Cell编号按SN着色
ax.scatter(group['TestTime_dt'], group['Measurement_num'],
color='blue', alpha=0.7, s=30, label=f"SN: {sn}")
@@ -1034,7 +1210,15 @@ class MultiFileTestReportScatterPlotter:
# 转换为base64
plot_image = self._plot_to_base64(fig)
sn_plots.append({"sn": str(sn), "image": plot_image})
sn_plots.append({
"sn": str(sn),
"image": plot_image,
"cell_info": None,
"has_cell_data": False,
"cell_no": None,
"sn_no": str(sn)
})
return sn_plots