import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from datetime import datetime, timedelta import logging logger = logging.getLogger(__name__) class JiraVisualizer: """ Клас для створення візуалізацій даних Jira """ def __init__(self, df): """ Ініціалізація візуалізатора. Args: df (pandas.DataFrame): DataFrame з даними Jira """ self.df = df self._setup_plot_style() def _setup_plot_style(self): """ Налаштування стилю візуалізацій. """ plt.style.use('ggplot') sns.set(style="whitegrid") # Налаштування для українських символів plt.rcParams['font.family'] = 'DejaVu Sans' def plot_status_counts(self): """ Створення діаграми розподілу тікетів за статусами. Returns: matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки """ try: if 'Status' not in self.df.columns: logger.warning("Колонка 'Status' відсутня") return None status_counts = self.df['Status'].value_counts() # Створення діаграми fig, ax = plt.subplots(figsize=(10, 6)) # Спроба впорядкувати статуси логічно try: status_order = ['To Do', 'In Progress', 'In Review', 'Done', 'Closed'] available_statuses = [s for s in status_order if s in status_counts.index] other_statuses = [s for s in status_counts.index if s not in status_order] ordered_statuses = available_statuses + other_statuses status_counts = status_counts.reindex(ordered_statuses) except: pass bars = sns.barplot(x=status_counts.index, y=status_counts.values, ax=ax) # Додаємо підписи значень над стовпцями for i, v in enumerate(status_counts.values): ax.text(i, v + 0.5, str(v), ha='center') ax.set_title('Розподіл тікетів за статусами') ax.set_xlabel('Статус') ax.set_ylabel('Кількість') plt.xticks(rotation=45) plt.tight_layout() logger.info("Діаграма статусів успішно створена") return fig except Exception as e: logger.error(f"Помилка при створенні діаграми статусів: {e}") return None def plot_priority_counts(self): """ Створення діаграми розподілу тікетів за пріоритетами. Returns: matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки """ try: if 'Priority' not in self.df.columns: logger.warning("Колонка 'Priority' відсутня") return None priority_counts = self.df['Priority'].value_counts() # Створення діаграми fig, ax = plt.subplots(figsize=(10, 6)) # Спроба впорядкувати пріоритети логічно try: priority_order = ['Highest', 'High', 'Medium', 'Low', 'Lowest'] available_priorities = [p for p in priority_order if p in priority_counts.index] other_priorities = [p for p in priority_counts.index if p not in priority_order] ordered_priorities = available_priorities + other_priorities priority_counts = priority_counts.reindex(ordered_priorities) except: pass # Кольори для різних пріоритетів colors = ['#FF5555', '#FF9C5A', '#FFCC5A', '#5AFF96', '#5AC8FF'] if len(priority_counts) <= len(colors): bars = sns.barplot(x=priority_counts.index, y=priority_counts.values, ax=ax, palette=colors[:len(priority_counts)]) else: bars = sns.barplot(x=priority_counts.index, y=priority_counts.values, ax=ax) # Додаємо підписи значень над стовпцями for i, v in enumerate(priority_counts.values): ax.text(i, v + 0.5, str(v), ha='center') ax.set_title('Розподіл тікетів за пріоритетами') ax.set_xlabel('Пріоритет') ax.set_ylabel('Кількість') plt.xticks(rotation=45) plt.tight_layout() logger.info("Діаграма пріоритетів успішно створена") return fig except Exception as e: logger.error(f"Помилка при створенні діаграми пріоритетів: {e}") return None def plot_type_counts(self): """ Створення діаграми розподілу тікетів за типами. Returns: matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки """ try: if 'Issue Type' not in self.df.columns: logger.warning("Колонка 'Issue Type' відсутня") return None type_counts = self.df['Issue Type'].value_counts() # Створення діаграми fig, ax = plt.subplots(figsize=(10, 6)) bars = sns.barplot(x=type_counts.index, y=type_counts.values, ax=ax) # Додаємо підписи значень над стовпцями for i, v in enumerate(type_counts.values): ax.text(i, v + 0.5, str(v), ha='center') ax.set_title('Розподіл тікетів за типами') ax.set_xlabel('Тип') ax.set_ylabel('Кількість') plt.xticks(rotation=45) plt.tight_layout() logger.info("Діаграма типів успішно створена") return fig except Exception as e: logger.error(f"Помилка при створенні діаграми типів: {e}") return None def plot_created_timeline(self): """ Створення часової діаграми створення тікетів. Returns: matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки """ try: if 'Created' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Created']): logger.warning("Колонка 'Created' відсутня або не містить дат") return None # Додаємо колонку з датою створення (без часу) if 'Created_Date' not in self.df.columns: self.df['Created_Date'] = self.df['Created'].dt.date # Кількість створених тікетів за датами created_by_date = self.df['Created_Date'].value_counts().sort_index() # Створення діаграми fig, ax = plt.subplots(figsize=(12, 6)) created_by_date.plot(kind='line', marker='o', ax=ax) ax.set_title('Кількість створених тікетів за датами') ax.set_xlabel('Дата') ax.set_ylabel('Кількість') ax.grid(True) plt.tight_layout() logger.info("Часова діаграма успішно створена") return fig except Exception as e: logger.error(f"Помилка при створенні часової діаграми: {e}") return None def plot_inactive_issues(self, days=14): """ Створення діаграми неактивних тікетів. Args: days (int): Кількість днів неактивності Returns: matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки """ try: if 'Updated' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Updated']): logger.warning("Колонка 'Updated' відсутня або не містить дат") return None # Визначення неактивних тікетів cutoff_date = datetime.now() - timedelta(days=days) inactive_issues = self.df[self.df['Updated'] < cutoff_date] if len(inactive_issues) == 0: logger.warning("Немає неактивних тікетів для візуалізації") return None # Розподіл неактивних тікетів за статусами if 'Status' in inactive_issues.columns: inactive_by_status = inactive_issues['Status'].value_counts() # Створення діаграми fig, ax = plt.subplots(figsize=(10, 6)) bars = sns.barplot(x=inactive_by_status.index, y=inactive_by_status.values, ax=ax) # Додаємо підписи значень над стовпцями for i, v in enumerate(inactive_by_status.values): ax.text(i, v + 0.5, str(v), ha='center') ax.set_title(f'Розподіл неактивних тікетів за статусами (>{days} днів)') ax.set_xlabel('Статус') ax.set_ylabel('Кількість') plt.xticks(rotation=45) plt.tight_layout() logger.info("Діаграма неактивних тікетів успішно створена") return fig else: logger.warning("Колонка 'Status' відсутня для неактивних тікетів") return None except Exception as e: logger.error(f"Помилка при створенні діаграми неактивних тікетів: {e}") return None def plot_status_timeline(self, timeline_df=None): """ Створення діаграми зміни статусів з часом. Args: timeline_df (pandas.DataFrame): DataFrame з часовими даними або None для автоматичного генерування Returns: matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки """ try: if timeline_df is None: if 'Created' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Created']): logger.warning("Колонка 'Created' відсутня або не містить дат") return None if 'Updated' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Updated']): logger.warning("Колонка 'Updated' відсутня або не містить дат") return None # Визначення часового діапазону min_date = self.df['Created'].min().date() max_date = self.df['Updated'].max().date() # Створення часового ряду для кожного дня date_range = pd.date_range(start=min_date, end=max_date, freq='D') # Збір статистики для кожної дати timeline_data = [] for date in date_range: date_str = date.strftime('%Y-%m-%d') # Тікети, створені до цієї дати created_until = self.df[self.df['Created'].dt.date <= date.date()] # Статуси тікетів на цю дату status_counts = {} # Для кожного тікета визначаємо його статус на цю дату for _, row in created_until.iterrows(): # Якщо тікет був оновлений після цієї дати, використовуємо його поточний статус if row['Updated'].date() >= date.date(): status = row.get('Status', 'Unknown') status_counts[status] = status_counts.get(status, 0) + 1 # Додаємо запис для цієї дати timeline_data.append({ 'Date': date_str, 'Total': len(created_until), **status_counts }) # Створення DataFrame timeline_df = pd.DataFrame(timeline_data) # Конвертація Date до datetime timeline_df['Date'] = pd.to_datetime(timeline_df['Date']) else: # Конвертація Date до datetime, якщо потрібно if not pd.api.types.is_datetime64_dtype(timeline_df['Date']): timeline_df['Date'] = pd.to_datetime(timeline_df['Date']) # Отримання статусів (всі колонки, крім Date і Total) status_columns = [col for col in timeline_df.columns if col not in ['Date', 'Total']] if not status_columns: logger.warning("Немає даних про статуси для візуалізації") return None # Створення діаграми fig, ax = plt.subplots(figsize=(14, 8)) # Створення сетплоту для статусів status_data = timeline_df[['Date'] + status_columns].set_index('Date') status_data.plot.area(ax=ax, stacked=True, alpha=0.7) ax.set_title('Зміна статусів тікетів з часом') ax.set_xlabel('Дата') ax.set_ylabel('Кількість тікетів') ax.grid(True) plt.tight_layout() logger.info("Часова діаграма статусів успішно створена") return fig except Exception as e: logger.error(f"Помилка при створенні часової діаграми статусів: {e}") return None def plot_lead_time_by_type(self): """ Створення діаграми часу виконання за типами тікетів. Returns: matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки """ try: if 'Created' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Created']): logger.warning("Колонка 'Created' відсутня або не містить дат") return None if 'Resolved' not in self.df.columns: logger.warning("Колонка 'Resolved' відсутня") return None if 'Issue Type' not in self.df.columns: logger.warning("Колонка 'Issue Type' відсутня") return None # Конвертація колонки Resolved до datetime, якщо потрібно if not pd.api.types.is_datetime64_dtype(self.df['Resolved']): self.df['Resolved'] = pd.to_datetime(self.df['Resolved'], errors='coerce') # Фільтрація завершених тікетів completed_issues = self.df.dropna(subset=['Resolved']) if len(completed_issues) == 0: logger.warning("Немає завершених тікетів для аналізу") return None # Обчислення Lead Time (в днях) completed_issues['Lead_Time_Days'] = (completed_issues['Resolved'] - completed_issues['Created']).dt.days # Фільтрація некоректних значень valid_lead_time = completed_issues[completed_issues['Lead_Time_Days'] >= 0] if len(valid_lead_time) == 0: logger.warning("Немає валідних даних про час виконання") return None # Обчислення середнього часу виконання за типами lead_time_by_type = valid_lead_time.groupby('Issue Type')['Lead_Time_Days'].mean() # Створення діаграми fig, ax = plt.subplots(figsize=(10, 6)) bars = sns.barplot(x=lead_time_by_type.index, y=lead_time_by_type.values, ax=ax) # Додаємо підписи значень над стовпцями for i, v in enumerate(lead_time_by_type.values): ax.text(i, v + 0.5, f"{v:.1f}", ha='center') ax.set_title('Середній час виконання тікетів за типами (дні)') ax.set_xlabel('Тип') ax.set_ylabel('Дні') plt.xticks(rotation=45) plt.tight_layout() logger.info("Діаграма часу виконання успішно створена") return fig except Exception as e: logger.error(f"Помилка при створенні діаграми часу виконання: {e}") return None def plot_all(self, output_dir=None): """ Створення та збереження всіх діаграм. Args: output_dir (str): Директорія для збереження діаграм. Якщо None, діаграми не зберігаються. Returns: dict: Словник з об'єктами figure для всіх діаграм """ plots = {} # Створення діаграм plots['status'] = self.plot_status_counts() plots['priority'] = self.plot_priority_counts() plots['type'] = self.plot_type_counts() plots['created_timeline'] = self.plot_created_timeline() plots['inactive'] = self.plot_inactive_issues() plots['status_timeline'] = self.plot_status_timeline() plots['lead_time'] = self.plot_lead_time_by_type() # Збереження діаграм, якщо вказана директорія if output_dir: import os from pathlib import Path # Створення директорії, якщо вона не існує output_path = Path(output_dir) output_path.mkdir(exist_ok=True, parents=True) # Збереження кожної діаграми for name, fig in plots.items(): if fig: fig_path = output_path / f"{name}.png" fig.savefig(fig_path, dpi=300) logger.info(f"Діаграма {name} збережена у {fig_path}") return plots