import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.ticker import FuncFormatter
from datetime import datetime, timedelta
import os
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import matplotlib
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import re
import matplotlib.colors as mcolors
import numpy as np
# 设置matplotlib支持中文显示
matplotlib.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]
matplotlib.rcParams["axes.unicode_minus"] = False # 解决负号显示问题
matplotlib.rcParams['svg.fonttype'] = 'path' # 确保SVG中正确嵌入字体路径
class LogParser:
"""日志解析器,支持更灵活的任务匹配规则"""
def __init__(self):
self.pattern = re.compile(
r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}:\d+)\] '
r'\[Thread-\d+\] '
r'\[Level-INFO\] '
r'\[Namespace-\S+\] '
r'\[Class-\S+\] '
r'\[Line-\d+\] '
r'VisualInspectRun线程([^:]+): (.*), @耗时: (\d+)' # 捕获类别和具体描述
)
self.target_categories = ['InspectImage', 'CalibQueue', 'SnapQueue']
def parse_log_data(self, log_data):
tasks = []
for line in log_data.split('\n'):
line = line.strip()
if not line:
continue
match = self.pattern.search(line)
if match:
timestamp_str, category, task_desc, duration_str = match.groups()
duration = int(duration_str)
completion_time = self._parse_timestamp(timestamp_str)
start_time = completion_time - timedelta(milliseconds=duration)
# 只保留逗号前的部分作为任务名称
task_name = f"{category}: {task_desc.split(',')[0].strip()}"
tasks.append({
'timestamp': completion_time,
'task_name': task_name, # 保存简化后的任务名称
'task_category': category, # 保存类别(用于着色)
'duration': duration,
'start_time': start_time,
'end_time': completion_time
})
return tasks
def _extract_category(self, task_name):
for category in self.target_categories:
if category in task_name:
return category
return None
def _parse_timestamp(self, timestamp_str):
parts = timestamp_str.split(':')
if len(parts) == 4:
timestamp_str = ':'.join(parts[:-1]) + '.' + parts[-1]
return datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S.%f')
class GanttChartGenerator:
"""甘特图生成器,使用高级配色方案"""
def __init__(self):
# 高级配色方案 - 使用国际专业图表配色标准(基于Pantone色系)
self.category_base_colors = {
'SnapQueue': '#005F9F', # 潘通 2945C(科技蓝)
'CalibQueue': '#00802B', # 潘通 3425C(森林绿)
'InspectImage': '#6A3D9A', # 潘通 2685C(贵族紫)
}
# 创建单色系渐变(保留2种深度)
self.category_palettes = {}
for category, base_color in self.category_base_colors.items():
base_rgb = mcolors.hex2color(base_color)
colors = [
base_color, # 主色
mcolors.rgb2hex(np.clip([c * 0.8 for c in base_rgb], 0, 1)) # 深色阴影
]
self.category_palettes[category] = colors
# 背景色方案 - 使用对应色系的10%明度浅色(基于WCAG标准)
self.background_colors = {
'SnapQueue': (0.85, 0.92, 0.98, 0.2), # 淡蓝色
'CalibQueue': (0.88, 0.96, 0.90, 0.2), # 淡绿色
'InspectImage': (0.93, 0.89, 0.97, 0.2) # 淡紫色
}
self.tasks = []
self.task_to_y = {} # 存储任务到Y轴位置的映射
self.task_colors = {} # 存储每个任务的颜色
self.task_to_category = {} # 任务到类别的映射
def generate_gantt_chart(self, tasks, figsize=(24, 12), dpi=150):
"""生成甘特图"""
self.tasks = tasks
if not tasks:
return None
df = pd.DataFrame(tasks)
if df.empty:
return None
# 建立任务到类别的映射
self.task_to_category = {task['task_name']: task['task_category'] for task in tasks}
# 定义排序优先级(按前缀顺序)
priority = {'SnapQueue': 0, 'CalibQueue': 1, 'InspectImage': 2}
# 按Y轴显示顺序排序任务
sorted_tasks = sorted(
{task['task_name'] for task in tasks},
key=lambda x: (priority.get(self.task_to_category[x], 3), x)
)[::-1] # 反转任务顺序
# 为每个任务分配唯一颜色(相邻行颜色不同)
self._assign_task_colors(sorted_tasks)
# 自定义行间距
spacing = 0.6
y_positions = [i * spacing for i in range(len(sorted_tasks))]
self.task_to_y = {task: y for y, task in zip(y_positions, sorted_tasks)} # 任务与Y轴位置映射
# 创建画布
fig, ax = plt.subplots(figsize=figsize, dpi=dpi)
ax.set_yticks(y_positions)
ax.set_yticklabels(sorted_tasks, fontsize=10, va='center') # 垂直居中对齐
# 绘制任务条(使用为每个任务分配的唯一颜色)
bar_height = 0.45
for idx, task in df.iterrows():
y_pos = self.task_to_y.get(task['task_name'], 0)
start_time = mdates.date2num(task['start_time'])
end_time = mdates.date2num(task['end_time'])
duration = end_time - start_time
ax.barh(
y_pos,
duration,
left=start_time,
height=bar_height,
align='center',
color=self.task_colors[task['task_name']],
alpha=0.9,
edgecolor='white',
linewidth=0.8,
zorder=3
)
# 计算文本偏移量
offset = min(0.05, duration * 0.1)
# 在任务条上显示耗时
if duration < 0.1:
ax.text(
start_time + duration/2,
y_pos,
f"{task['duration']}ms",
ha='center',
va='bottom',
fontsize=7,
color='darkred',
zorder=4
)
else:
ax.text(
end_time - offset,
y_pos,
f"{task['duration']}ms",
ha='right',
va='center',
fontsize=7,
color='darkred',
zorder=4,
bbox=dict(boxstyle='round,pad=0.1', facecolor='white', alpha=0.7, edgecolor='none')
)
# 添加背景颜色区域
self._add_background_colors(ax, df)
# 时间轴设置
def time_format(x, pos):
dt = mdates.num2date(x)
return dt.strftime('%H:%M:%S.%f')[:-3] # 显示3位毫秒
ax.xaxis.set_major_formatter(FuncFormatter(time_format))
ax.xaxis.set_major_locator(mdates.MicrosecondLocator(interval=500000)) # 500ms间隔
ax.xaxis.set_minor_locator(mdates.MicrosecondLocator(interval=100000)) # 100ms间隔
ax.tick_params(axis='x', labelsize=9, rotation=45)
ax.set_xlabel('时间 (时:分:秒:毫秒)', fontsize=11)
ax.set_ylabel('任务', fontsize=11)
ax.set_title('任务执行甘特图 - 耗时节拍分析', fontsize=16, pad=15, fontweight='bold')
# 设置网格线
ax.grid(True, which='major', linestyle='-', linewidth=0.7, alpha=0.3, color='#aaaaaa')
ax.grid(True, which='minor', linestyle=':', linewidth=0.5, alpha=0.2, color='#cccccc')
# 设置背景颜色
ax.set_facecolor('#f8f9fa')
fig.set_facecolor('#ffffff')
# 更新图例为每行任务名称(使用每行的颜色)
self._update_legend(ax, sorted_tasks)
# 添加时间范围和总时长信息
min_time = df['start_time'].min()
max_time = df['end_time'].max()
total_duration = (max_time - min_time).total_seconds() * 1000
# 添加统计信息框
stats_text = f"时间范围: {min_time.strftime('%H:%M:%S.%f')[:-3]} - {max_time.strftime('%H:%M:%S.%f')[:-3]}\n"
stats_text += f"总时长: {total_duration:.0f}ms\n"
stats_text += f"任务数量: {len(tasks)}"
ax.text(0.99, 0.01,
stats_text,
transform=ax.transAxes,
ha='right',
va='bottom',
fontsize=9,
bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.5', edgecolor='#dddddd'))
plt.tight_layout(pad=3)
return fig
def _assign_task_colors(self, sorted_tasks):
"""为每个任务分配颜色(相邻行颜色不同,同一类别使用同色系)"""
category_color_map = {} # 记录每个类别已分配的颜色索引
for idx, task in enumerate(sorted_tasks):
category = self.task_to_category[task]
if category not in category_color_map:
# 初始化类别颜色索引(从0开始)
category_color_map[category] = 0
else:
# 切换颜色索引(0和1交替)
category_color_map[category] = 1 - category_color_map[category]
# 为任务分配颜色(根据索引选择主色或深色阴影)
self.task_colors[task] = self.category_palettes[category][category_color_map[category]]
def _add_background_colors(self, ax, df):
"""为不同类别的任务添加背景颜色区域"""
for category, color in self.background_colors.items():
# 筛选出包含当前类别的任务
filtered_tasks = [task for task in self.tasks if task['task_category'] == category]
if not filtered_tasks:
continue
# 获取这些任务在Y轴的位置范围
task_names = [task['task_name'] for task in filtered_tasks]
y_indices = [self.task_to_y.get(task, 0) for task in task_names if task in self.task_to_y]
if not y_indices:
continue
y_min = min(y_indices) - 0.3
y_max = max(y_indices) + 0.3
# 绘制横向背景条(降低透明度,避免干扰任务条区分)
ax.axhspan(y_min, y_max, xmin=0, xmax=1, facecolor=color, zorder=0, alpha=0.1)
def _update_legend(self, ax, sorted_tasks):
"""更新图例,显示每行任务的名称和颜色"""
# 为每个任务创建图例项,使用其对应的颜色
legend_elements = []
for task in reversed(sorted_tasks): # 添加 reversed() 来反转顺序,保持图例与图表顺序一致
legend_elements.append(plt.Rectangle((0, 0), 1, 1,
color=self.task_colors[task],
label=task))
# 添加图例(按Y轴顺序)
ax.legend(
handles=legend_elements,
loc='upper left',
bbox_to_anchor=(1.01, 1.0),
title='任务列表',
fontsize=8,
title_fontsize=10,
framealpha=0.9,
borderpad=0.8,
labelspacing=0.7,
handlelength=1.8,
handleheight=1.2,
frameon=True,
fancybox=True,
shadow=False,
edgecolor='#dddddd'
)
def save_to_excel(self, file_path):
if not self.tasks:
return False
df = pd.DataFrame(self.tasks)
with pd.ExcelWriter(file_path, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='任务数据', index=False)
workbook = writer.book
worksheet = writer.sheets['任务数据']
for i, col in enumerate(df.columns):
column_width = max(len(str(x)) for x in df[col])
column_width = max(column_width, len(col)) + 2
col_letter = chr(65 + i)
worksheet.column_dimensions[col_letter].width = column_width
for col_idx, col_name in enumerate(df.columns):
if col_name in ['timestamp', 'start_time', 'end_time']:
col_letter = chr(65 + col_idx)
for row_idx in range(2, len(df) + 2):
cell = f"{col_letter}{row_idx}"
worksheet[cell].number_format = 'yyyy-mm-dd hh:mm:ss.000'
return True
def save_to_svg(self, fig, file_path):
"""将图表保存为优化后的SVG文件"""
if fig is None:
return False
try:
fig.savefig(file_path, format='svg', bbox_inches='tight', metadata={
'Title': '任务执行甘特图',
'Creator': '高级甘特图生成器',
'Description': '专业级任务执行时序分析'
})
with open(file_path, 'r', encoding='utf-8') as f:
svg_content = f.read()
svg_content = self._optimize_svg(svg_content)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(svg_content)
return True
except Exception as e:
print(f"保存SVG时出错: {str(e)}")
return False
def _optimize_svg(self, svg_content):
"""优化SVG内容以减小文件大小并确保兼容性"""
if 'viewBox="' not in svg_content:
width_match = re.search(r'width="(\d+(\.\d+)?)(pt|px|cm|mm)"', svg_content)
height_match = re.search(r'height="(\d+(\.\d+)?)(pt|px|cm|mm)"', svg_content)
if width_match and height_match:
width = float(width_match.group(1))
height = float(height_match.group(1))
viewbox = f'viewBox="0 0 {width} {height}"'
svg_content = re.sub(r'(<svg[^>]*)(>)', fr'\1 {viewbox}\2', svg_content, count=1)
desc = '<desc>Generated by Professional Gantt Chart Generator</desc>'
title = '<title>Task Execution Gantt Chart</title>'
svg_content = svg_content.replace('<svg', f'<svg xmlns="http://www.w3.org/2000/svg">\n{title}\n{desc}\n', 1)
svg_content = re.sub(r'<style type="text/css">\s*([^<]+)\s*</style>', r'<style>\1</style>', svg_content)
return svg_content
class GanttChartApp:
"""甘特图应用程序主类,提供图形界面"""
def __init__(self, root):
self.root = root
self.root.title("甘特图生成器 - 专业耗时节拍分析")
self.root.geometry("1200x800")
self.root.minsize(1000, 650)
self.root.configure(bg='#f5f7fa') # 设置背景色
# 设置主题
self.style = ttk.Style()
self.style.theme_use('clam') # 使用现代主题
# 自定义样式
self.style.configure('TFrame', background='#f5f7fa')
self.style.configure('TLabel', background='#f5f7fa', font=('Segoe UI', 10))
self.style.configure('TLabelframe', background='#f5f7fa', font=('Segoe UI', 10, 'bold'))
self.style.configure('TLabelframe.Label', background='#f5f7fa', font=('Segoe UI', 10, 'bold'))
self.style.configure('TButton', font=('Segoe UI', 10), padding=6, background='#4a6fa5', foreground='white')
self.style.map('TButton',
background=[('active', '#3a5a80'), ('pressed', '#2a4a70')],
foreground=[('active', 'white'), ('pressed', 'white')])
self.style.configure('TProgressbar', thickness=15, background='#4a6fa5')
self.log_parser = LogParser()
self.gantt_generator = GanttChartGenerator()
self.tasks = []
self.fig = None
self.create_widgets()
def create_widgets(self):
"""创建GUI组件"""
main_frame = ttk.Frame(self.root, padding=15)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 标题
header_frame = ttk.Frame(main_frame)
header_frame.pack(fill=tk.X, pady=(0, 15))
title_label = ttk.Label(header_frame, text="专业甘特图生成器",
font=('Segoe UI', 16, 'bold'),
foreground='#2c3e50')
title_label.pack(side=tk.LEFT)
subtitle_label = ttk.Label(header_frame, text="任务执行时序分析工具",
font=('Segoe UI', 11),
foreground='#7f8c8d')
subtitle_label.pack(side=tk.LEFT, padx=(10, 0))
# 日志输入区域
input_frame = ttk.LabelFrame(main_frame, text="日志输入", padding=15)
input_frame.pack(fill=tk.X, pady=(0, 15))
ttk.Label(input_frame, text="日志内容:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.log_text = tk.Text(input_frame, height=10, width=80, font=("Consolas", 10),
bg='white', highlightbackground='#dce1e8', highlightthickness=1)
self.log_text.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
scrollbar = ttk.Scrollbar(input_frame, command=self.log_text.yview)
scrollbar.grid(row=1, column=3, sticky=(tk.N, tk.S))
self.log_text.config(yscrollcommand=scrollbar.set)
# 按钮区域
btn_frame = ttk.Frame(input_frame)
btn_frame.grid(row=2, column=0, columnspan=4, pady=10)
load_btn = ttk.Button(btn_frame, text="加载日志文件", command=self.load_log_file)
load_btn.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
generate_btn = ttk.Button(btn_frame, text="生成并保存甘特图", command=self.generate_and_save_chart)
generate_btn.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
excel_btn = ttk.Button(btn_frame, text="保存为Excel", command=self.save_to_excel)
excel_btn.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
exit_btn = ttk.Button(btn_frame, text="退出", command=self.root.quit)
exit_btn.pack(side=tk.RIGHT, padx=5, fill=tk.X, expand=True)
# 进度条
self.progress = ttk.Progressbar(btn_frame, orient=tk.HORIZONTAL, length=200, mode='determinate')
self.progress.pack(side=tk.LEFT, padx=20, fill=tk.X, expand=True)
# 状态区域
self.status_var = tk.StringVar()
self.status_var.set("就绪 - 请输入日志内容或加载日志文件")
status_label = ttk.Label(input_frame, textvariable=self.status_var,
font=('Segoe UI', 9), foreground='#7f8c8d')
status_label.grid(row=3, column=0, sticky=tk.W, pady=5)
# 结果输出区域
result_frame = ttk.LabelFrame(main_frame, text="操作结果", padding=15)
result_frame.pack(fill=tk.BOTH, expand=True)
self.result_text = tk.Text(result_frame, height=10, width=80,
font=("Segoe UI", 10), wrap=tk.WORD,
bg='white', highlightbackground='#dce1e8', highlightthickness=1)
self.result_text.pack(fill=tk.BOTH, expand=True, pady=5)
self.result_text.config(state=tk.DISABLED)
# 底部状态栏
footer_frame = ttk.Frame(main_frame)
footer_frame.pack(fill=tk.X, pady=(10, 0))
version_label = ttk.Label(footer_frame, text="版本 1.2.0 | 专业甘特图生成器",
font=('Segoe UI', 8), foreground='#95a5a6')
version_label.pack(side=tk.RIGHT)
def load_log_file(self):
file_path = filedialog.askopenfilename(
title="选择日志文件",
filetypes=[("文本文件", "*.txt"), ("日志文件", "*.log"), ("所有文件", "*.*")]
)
if not file_path:
return
try:
self.progress['value'] = 0
self.root.update()
with open(file_path, 'r', encoding='utf-8') as file:
log_data = file.read()
self.log_text.delete(1.0, tk.END)
self.log_text.insert(tk.END, log_data)
self.status_var.set(f"已加载文件: {os.path.basename(file_path)}")
self.progress['value'] = 100
self.update_result_text(f"已加载日志文件: {os.path.basename(file_path)}")
self.update_result_text(f"日志大小: {len(log_data)} 字节")
self.update_result_text(f"日志行数: {len(log_data.splitlines())} 行")
except Exception as e:
messagebox.showerror("错误", f"加载文件失败: {str(e)}")
self.progress['value'] = 0
def generate_and_save_chart(self):
log_data = self.log_text.get(1.0, tk.END).strip()
if not log_data:
messagebox.showwarning("警告", "请输入日志内容或加载日志文件")
return
try:
self.progress['value'] = 0
self.status_var.set("正在解析日志...")
self.root.update()
self.tasks = self.log_parser.parse_log_data(log_data)
self.progress['value'] = 30
if not self.tasks:
messagebox.showwarning("警告", "未能从日志中解析出任务数据,请检查日志格式")
self.progress['value'] = 0
return
self.update_result_text(f"已解析任务: {len(self.tasks)} 个")
self.update_result_text(f"任务类别: {', '.join(set(t['task_category'] for t in self.tasks))}")
self.status_var.set("正在生成甘特图...")
self.root.update()
self.fig = self.gantt_generator.generate_gantt_chart(self.tasks)
self.progress['value'] = 70
if self.fig is None:
messagebox.showwarning("警告", "生成甘特图失败")
self.progress['value'] = 0
return
file_path = filedialog.asksaveasfilename(
title="保存甘特图",
defaultextension=".svg",
filetypes=[("SVG文件", "*.svg"), ("PNG文件", "*.png"), ("所有文件", "*.*")]
)
if not file_path:
self.progress['value'] = 0
return
self.status_var.set(f"正在保存甘特图到 {os.path.basename(file_path)}...")
self.root.update()
if file_path.lower().endswith('.svg'):
if self.gantt_generator.save_to_svg(self.fig, file_path):
self.progress['value'] = 100
self.status_var.set(f"已成功保存甘特图: {os.path.basename(file_path)}")
self.update_result_text(f"甘特图已保存为: {file_path}")
messagebox.showinfo("成功", f"甘特图已保存到: {file_path}")
else:
self.progress['value'] = 0
messagebox.showerror("错误", "保存SVG文件失败")
elif file_path.lower().endswith(('.png', '.jpg', '.jpeg')):
try:
self.fig.savefig(file_path, dpi=300, bbox_inches='tight')
self.progress['value'] = 100
self.status_var.set(f"已成功保存甘特图: {os.path.basename(file_path)}")
self.update_result_text(f"甘特图已保存为: {file_path}")
messagebox.showinfo("成功", f"甘特图已保存到: {file_path}")
except Exception as e:
self.progress['value'] = 0
messagebox.showerror("错误", f"保存图像文件失败: {str(e)}")
else:
self.progress['value'] = 0
messagebox.showwarning("警告", "不支持的文件格式,请选择.svg或.png格式")
except Exception as e:
messagebox.showerror("错误", f"生成甘特图失败: {str(e)}")
self.progress['value'] = 0
import traceback
traceback.print_exc()
def save_to_excel(self):
if not self.tasks:
messagebox.showwarning("警告", "没有任务数据可保存")
return
file_path = filedialog.asksaveasfilename(
title="保存为Excel",
defaultextension=".xlsx",
filetypes=[("Excel文件", "*.xlsx"), ("所有文件", "*.*")]
)
if not file_path:
return
try:
self.progress['value'] = 0
self.status_var.set("正在保存Excel文件...")
self.root.update()
if self.gantt_generator.save_to_excel(file_path):
self.progress['value'] = 100
self.status_var.set(f"已成功保存Excel文件: {os.path.basename(file_path)}")
self.update_result_text(f"任务数据已保存为: {file_path}")
messagebox.showinfo("成功", f"Excel文件已保存到: {file_path}")
else:
self.progress['value'] = 0
messagebox.showerror("错误", "保存Excel文件失败")
except Exception as e:
self.progress['value'] = 0
messagebox.showerror("错误", f"保存Excel文件失败: {str(e)}")
def update_result_text(self, message):
self.result_text.config(state=tk.NORMAL)
self.result_text.insert(tk.END, message + "\n")
self.result_text.see(tk.END)
self.result_text.config(state=tk.DISABLED)
if __name__ == "__main__":
root = tk.Tk()
app = GanttChartApp(root)
root.mainloop()
python
于 2025-06-04 17:50:11 首次发布

4268

被折叠的 条评论
为什么被折叠?



