|
1 | 1 | # stats_window.py
|
2 | 2 |
|
3 |
| -import tkinter as tk |
4 | 3 | import pandas as pd
|
5 | 4 | import matplotlib.pyplot as plt
|
6 |
| -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg |
| 5 | +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas |
7 | 6 | import os
|
8 | 7 | import ast
|
9 | 8 | import datetime
|
| 9 | +import numpy as np |
| 10 | +from PySide6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, |
| 11 | + QPushButton, QLabel, QTabWidget, QTableWidget, |
| 12 | + QTableWidgetItem, QFrame, QScrollArea, QTextEdit) |
| 13 | +from PySide6.QtCore import Qt, QThread, Signal |
| 14 | +from PySide6.QtGui import QFont |
10 | 15 |
|
11 | 16 | from utils import read_gitignore, is_ignored, find_projects
|
12 | 17 |
|
@@ -50,8 +55,6 @@ def analyze_project_structure(directory, task_queue):
|
50 | 55 | task_queue.put(('project_stats', structure))
|
51 | 56 |
|
52 | 57 |
|
53 |
| - |
54 |
| - |
55 | 58 | def parse_python_files(projects_dir, export=True, max_files=5000, max_depth=6):
|
56 | 59 | import os, ast, datetime
|
57 | 60 | import pandas as pd
|
@@ -153,83 +156,269 @@ def parse_python_files(projects_dir, export=True, max_files=5000, max_depth=6):
|
153 | 156 | return df.to_dict("records")
|
154 | 157 |
|
155 | 158 |
|
156 |
| - |
157 |
| - |
158 |
| - |
159 |
| -def open_stats_window(root, project_data: list[dict]): |
160 |
| - stats_win = tk.Toplevel(root) |
161 |
| - stats_win.title("📊 Статистика по проектам") |
162 |
| - stats_win.geometry("1000x700") |
163 |
| - |
164 |
| - if not project_data: |
165 |
| - tk.Label(stats_win, text="❌ Нет данных для отображения.", font=("Arial", 12)).pack(pady=20) |
166 |
| - return |
167 |
| - |
168 |
| - df = pd.DataFrame(project_data) |
169 |
| - |
170 |
| - if df.empty or 'date' not in df.columns: |
171 |
| - tk.Label(stats_win, text="❌ Недостаточно данных для статистики.", font=("Arial", 12)).pack(pady=20) |
172 |
| - return |
173 |
| - |
174 |
| - df['date'] = pd.to_datetime(df['date'], errors='coerce') |
175 |
| - df = df.dropna(subset=['date']) |
176 |
| - |
177 |
| - if df.empty: |
178 |
| - tk.Label(stats_win, text="❌ Все даты повреждены или отсутствуют.", font=("Arial", 12)).pack(pady=20) |
179 |
| - return |
180 |
| - |
181 |
| - df['month'] = df['date'].dt.to_period('M') |
182 |
| - |
183 |
| - # === Верхняя сводка === |
184 |
| - summary_frame = tk.Frame(stats_win) |
185 |
| - summary_frame.pack(pady=10) |
186 |
| - |
187 |
| - total_projects = len(df) |
188 |
| - total_files = df['py_count'].sum() |
189 |
| - earliest = df['date'].min().strftime("%Y-%m-%d") |
190 |
| - latest = df['date'].max().strftime("%Y-%m-%d") |
191 |
| - unique_months = df['month'].nunique() |
192 |
| - |
193 |
| - summary_text = ( |
194 |
| - f"📦 Всего проектов: {total_projects} 🧠 Всего .py файлов: {total_files}\n" |
195 |
| - f"📆 Период: {earliest} → {latest} 📊 Уникальных месяцев: {unique_months}" |
196 |
| - ) |
197 |
| - tk.Label(summary_frame, text=summary_text, font=("Arial", 11), justify="left").pack() |
198 |
| - |
199 |
| - # === График по месяцам === |
200 |
| - chart_frame = tk.Frame(stats_win) |
201 |
| - chart_frame.pack(fill="both", expand=False, pady=5) |
202 |
| - |
203 |
| - fig, ax = plt.subplots(figsize=(8, 4)) |
204 |
| - df_by_month = df.groupby('month').size().sort_index() |
205 |
| - df_by_month.plot(kind='bar', ax=ax, color='skyblue', edgecolor='black') |
206 |
| - |
207 |
| - ax.set_title('📅 Количество проектов по месяцам', fontsize=14) |
208 |
| - ax.set_ylabel('Проекты', fontsize=12) |
209 |
| - ax.set_xlabel('Месяц', fontsize=12) |
210 |
| - ax.grid(True, axis='y', linestyle='--', alpha=0.5) |
211 |
| - fig.tight_layout() |
212 |
| - |
213 |
| - canvas = FigureCanvasTkAgg(fig, master=chart_frame) |
214 |
| - canvas.draw() |
215 |
| - canvas.get_tk_widget().pack() |
216 |
| - |
217 |
| - # === Анализ технологий === |
218 |
| - tech_frame = tk.LabelFrame(stats_win, text="📚 Используемые библиотеки", font=("Arial", 12)) |
219 |
| - tech_frame.pack(fill="both", expand=True, padx=10, pady=10) |
220 |
| - |
221 |
| - if 'stack' in df.columns: |
222 |
| - all_stacks = sum(df['stack'].tolist(), []) |
223 |
| - stack_series = pd.Series(all_stacks).value_counts() |
224 |
| - if not stack_series.empty: |
225 |
| - stack_text = "\n".join(f"• {lib:<20} — {count}" for lib, count in stack_series.items()) |
226 |
| - else: |
227 |
| - stack_text = "Нет данных о библиотеках." |
228 |
| - else: |
229 |
| - stack_text = "Нет данных по стеку технологий." |
230 |
| - |
231 |
| - tk.Message(tech_frame, text=stack_text, font=("Courier New", 10), width=900, anchor="w", justify="left").pack() |
232 |
| - |
233 |
| - # === Кнопка закрытия === |
234 |
| - tk.Button(stats_win, text="Закрыть", command=stats_win.destroy).pack(pady=10) |
| 159 | +class StatsWindow(QMainWindow): |
| 160 | + def __init__(self, imports_count, parent=None): |
| 161 | + super().__init__(parent) |
| 162 | + self.imports_count = imports_count |
| 163 | + self.init_ui() |
| 164 | + self.setup_styles() |
| 165 | + |
| 166 | + def init_ui(self): |
| 167 | + self.setWindowTitle("📊 Статистика по проектам") |
| 168 | + self.setGeometry(200, 200, 1000, 700) |
| 169 | + |
| 170 | + # Центральный виджет |
| 171 | + central_widget = QWidget() |
| 172 | + self.setCentralWidget(central_widget) |
| 173 | + |
| 174 | + # Главный layout |
| 175 | + main_layout = QVBoxLayout(central_widget) |
| 176 | + |
| 177 | + # Верхняя сводка |
| 178 | + summary_frame = QFrame() |
| 179 | + summary_layout = QVBoxLayout(summary_frame) |
| 180 | + |
| 181 | + self.summary_label = QLabel("📦 Анализ проектов") |
| 182 | + self.summary_label.setAlignment(Qt.AlignCenter) |
| 183 | + self.summary_label.setFont(QFont("Arial", 14, QFont.Bold)) |
| 184 | + summary_layout.addWidget(self.summary_label) |
| 185 | + |
| 186 | + main_layout.addWidget(summary_frame) |
| 187 | + |
| 188 | + # Табы для разных видов статистики |
| 189 | + self.tab_widget = QTabWidget() |
| 190 | + |
| 191 | + # Таб с графиками |
| 192 | + self.charts_tab = self.create_charts_tab() |
| 193 | + self.tab_widget.addTab(self.charts_tab, "📊 Графики") |
| 194 | + |
| 195 | + # Таб с таблицей |
| 196 | + self.table_tab = self.create_table_tab() |
| 197 | + self.tab_widget.addTab(self.table_tab, "📋 Таблица") |
| 198 | + |
| 199 | + # Таб с деталями |
| 200 | + self.details_tab = self.create_details_tab() |
| 201 | + self.tab_widget.addTab(self.details_tab, "📝 Детали") |
| 202 | + |
| 203 | + main_layout.addWidget(self.tab_widget) |
| 204 | + |
| 205 | + # Кнопки |
| 206 | + button_layout = QHBoxLayout() |
| 207 | + |
| 208 | + self.export_btn = QPushButton("💾 Экспорт в CSV") |
| 209 | + self.export_btn.clicked.connect(self.export_to_csv) |
| 210 | + |
| 211 | + self.close_btn = QPushButton("❌ Закрыть") |
| 212 | + self.close_btn.clicked.connect(self.close) |
| 213 | + |
| 214 | + button_layout.addWidget(self.export_btn) |
| 215 | + button_layout.addStretch() |
| 216 | + button_layout.addWidget(self.close_btn) |
| 217 | + |
| 218 | + main_layout.addLayout(button_layout) |
| 219 | + |
| 220 | + def setup_styles(self): |
| 221 | + """Настройка современного стиля""" |
| 222 | + self.setStyleSheet(""" |
| 223 | + QMainWindow { |
| 224 | + background-color: #2b2b2b; |
| 225 | + color: #ffffff; |
| 226 | + } |
| 227 | + QTabWidget::pane { |
| 228 | + border: 1px solid #444444; |
| 229 | + background-color: #1e1e1e; |
| 230 | + } |
| 231 | + QTabBar::tab { |
| 232 | + background-color: #3c3c3c; |
| 233 | + color: #ffffff; |
| 234 | + padding: 8px 16px; |
| 235 | + margin-right: 2px; |
| 236 | + } |
| 237 | + QTabBar::tab:selected { |
| 238 | + background-color: #4a90e2; |
| 239 | + } |
| 240 | + QPushButton { |
| 241 | + background-color: #4a90e2; |
| 242 | + color: white; |
| 243 | + border: none; |
| 244 | + padding: 8px 16px; |
| 245 | + border-radius: 4px; |
| 246 | + font-weight: bold; |
| 247 | + } |
| 248 | + QPushButton:hover { |
| 249 | + background-color: #357abd; |
| 250 | + } |
| 251 | + QLabel { |
| 252 | + color: #ffffff; |
| 253 | + } |
| 254 | + QTableWidget { |
| 255 | + background-color: #1e1e1e; |
| 256 | + color: #ffffff; |
| 257 | + gridline-color: #444444; |
| 258 | + } |
| 259 | + QTextEdit { |
| 260 | + background-color: #1e1e1e; |
| 261 | + color: #ffffff; |
| 262 | + border: 1px solid #444444; |
| 263 | + } |
| 264 | + """) |
| 265 | + |
| 266 | + def create_charts_tab(self): |
| 267 | + """Создание таба с графиками""" |
| 268 | + widget = QWidget() |
| 269 | + layout = QVBoxLayout(widget) |
| 270 | + |
| 271 | + # График импортов |
| 272 | + self.imports_chart = self.create_imports_chart() |
| 273 | + layout.addWidget(self.imports_chart) |
| 274 | + |
| 275 | + return widget |
| 276 | + |
| 277 | + def create_table_tab(self): |
| 278 | + """Создание таба с таблицей""" |
| 279 | + widget = QWidget() |
| 280 | + layout = QVBoxLayout(widget) |
| 281 | + |
| 282 | + self.table = QTableWidget() |
| 283 | + self.populate_table() |
| 284 | + layout.addWidget(self.table) |
| 285 | + |
| 286 | + return widget |
| 287 | + |
| 288 | + def create_details_tab(self): |
| 289 | + """Создание таба с деталями""" |
| 290 | + widget = QWidget() |
| 291 | + layout = QVBoxLayout(widget) |
| 292 | + |
| 293 | + self.details_text = QTextEdit() |
| 294 | + self.details_text.setReadOnly(True) |
| 295 | + self.populate_details() |
| 296 | + layout.addWidget(self.details_text) |
| 297 | + |
| 298 | + return widget |
| 299 | + |
| 300 | + def create_imports_chart(self): |
| 301 | + """Создание графика импортов""" |
| 302 | + if not self.imports_count: |
| 303 | + label = QLabel("Нет данных для отображения") |
| 304 | + label.setAlignment(Qt.AlignCenter) |
| 305 | + return label |
| 306 | + |
| 307 | + # Создаем график |
| 308 | + fig, ax = plt.subplots(figsize=(10, 6)) |
| 309 | + |
| 310 | + # Сортируем данные |
| 311 | + sorted_imports = sorted(self.imports_count.items(), |
| 312 | + key=lambda x: x[1], reverse=True)[:20] |
| 313 | + |
| 314 | + libraries = [lib for lib, _ in sorted_imports] |
| 315 | + counts = [count for _, count in sorted_imports] |
| 316 | + |
| 317 | + # Создаем горизонтальную гистограмму |
| 318 | + bars = ax.barh(libraries, counts, color='skyblue', edgecolor='black') |
| 319 | + |
| 320 | + # Настройки графика |
| 321 | + ax.set_xlabel('Количество использований') |
| 322 | + ax.set_title('Топ-20 используемых библиотек') |
| 323 | + ax.grid(True, axis='x', linestyle='--', alpha=0.7) |
| 324 | + |
| 325 | + # Добавляем значения на столбцы |
| 326 | + for i, (bar, count) in enumerate(zip(bars, counts)): |
| 327 | + ax.text(bar.get_width() + 0.1, bar.get_y() + bar.get_height()/2, |
| 328 | + str(count), va='center') |
| 329 | + |
| 330 | + plt.tight_layout() |
| 331 | + |
| 332 | + # Создаем canvas для Qt |
| 333 | + canvas = FigureCanvas(fig) |
| 334 | + return canvas |
| 335 | + |
| 336 | + def populate_table(self): |
| 337 | + """Заполнение таблицы данными""" |
| 338 | + if not self.imports_count: |
| 339 | + return |
| 340 | + |
| 341 | + # Настройка таблицы |
| 342 | + sorted_imports = sorted(self.imports_count.items(), |
| 343 | + key=lambda x: x[1], reverse=True) |
| 344 | + |
| 345 | + self.table.setRowCount(len(sorted_imports)) |
| 346 | + self.table.setColumnCount(3) |
| 347 | + self.table.setHorizontalHeaderLabels(['Библиотека', 'Количество', '%']) |
| 348 | + |
| 349 | + total = sum(self.imports_count.values()) |
| 350 | + |
| 351 | + for i, (lib, count) in enumerate(sorted_imports): |
| 352 | + percentage = (count / total) * 100 |
| 353 | + |
| 354 | + self.table.setItem(i, 0, QTableWidgetItem(lib)) |
| 355 | + self.table.setItem(i, 1, QTableWidgetItem(str(count))) |
| 356 | + self.table.setItem(i, 2, QTableWidgetItem(f"{percentage:.2f}%")) |
| 357 | + |
| 358 | + self.table.resizeColumnsToContents() |
| 359 | + |
| 360 | + def populate_details(self): |
| 361 | + """Заполнение детальной информации""" |
| 362 | + if not self.imports_count: |
| 363 | + self.details_text.setText("Нет данных для отображения") |
| 364 | + return |
| 365 | + |
| 366 | + total = sum(self.imports_count.values()) |
| 367 | + unique_libs = len(self.imports_count) |
| 368 | + |
| 369 | + details = f""" |
| 370 | +📊 ДЕТАЛЬНАЯ СТАТИСТИКА ИМПОРТОВ |
| 371 | +
|
| 372 | +📈 Общая информация: |
| 373 | +• Всего импортов: {total:,} |
| 374 | +• Уникальных библиотек: {unique_libs} |
| 375 | +• Среднее использование: {total/unique_libs:.1f} |
| 376 | +
|
| 377 | +🏆 Топ-10 библиотек: |
| 378 | +""" |
| 379 | + |
| 380 | + sorted_imports = sorted(self.imports_count.items(), |
| 381 | + key=lambda x: x[1], reverse=True)[:10] |
| 382 | + |
| 383 | + for i, (lib, count) in enumerate(sorted_imports, 1): |
| 384 | + percentage = (count / total) * 100 |
| 385 | + details += f"{i:2d}. {lib:<20} {count:>6} ({percentage:>5.1f}%)\n" |
| 386 | + |
| 387 | + self.details_text.setText(details) |
| 388 | + |
| 389 | + def export_to_csv(self): |
| 390 | + """Экспорт данных в CSV""" |
| 391 | + if not self.imports_count: |
| 392 | + return |
| 393 | + |
| 394 | + import pandas as pd |
| 395 | + |
| 396 | + # Создаем DataFrame |
| 397 | + data = [] |
| 398 | + total = sum(self.imports_count.values()) |
| 399 | + |
| 400 | + for lib, count in self.imports_count.items(): |
| 401 | + percentage = (count / total) * 100 |
| 402 | + data.append({ |
| 403 | + 'Библиотека': lib, |
| 404 | + 'Количество': count, |
| 405 | + 'Процент': round(percentage, 2) |
| 406 | + }) |
| 407 | + |
| 408 | + df = pd.DataFrame(data) |
| 409 | + df = df.sort_values('Количество', ascending=False) |
| 410 | + |
| 411 | + # Сохраняем файл |
| 412 | + filename = f"imports_analysis_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" |
| 413 | + df.to_csv(filename, index=False, encoding='utf-8-sig') |
| 414 | + |
| 415 | + from PySide6.QtWidgets import QMessageBox |
| 416 | + QMessageBox.information(self, "Экспорт", f"Данные сохранены в файл: {filename}") |
| 417 | + |
| 418 | + |
| 419 | +def open_stats_window(parent, imports_count): |
| 420 | + """Функция для открытия окна статистики""" |
| 421 | + stats_window = StatsWindow(imports_count, parent) |
| 422 | + stats_window.show() |
| 423 | + return stats_window |
235 | 424 |
|
0 commit comments