DocUA's picture
Покращена візуалізація рапортів
fb9ceee
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 Exception as ex:
logger.warning(f"Не вдалося впорядкувати статуси: {ex}")
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 Exception as ex:
logger.warning(f"Не вдалося впорядкувати пріоритети: {ex}")
colors = ['#FF5555', '#FF9C5A', '#FFCC5A', '#5AFF96', '#5AC8FF']
if len(priority_counts) <= len(colors):
sns.barplot(x=priority_counts.index, y=priority_counts.values, ax=ax, palette=colors[:len(priority_counts)])
else:
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))
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))
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
})
timeline_df = pd.DataFrame(timeline_data)
timeline_df['Date'] = pd.to_datetime(timeline_df['Date'])
else:
if not pd.api.types.is_datetime64_dtype(timeline_df['Date']):
timeline_df['Date'] = pd.to_datetime(timeline_df['Date'])
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
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
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))
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
# Нові методи, додані до класу JiraVisualizer
def plot_assignee_counts(self, limit=10):
"""
Створення діаграми розподілу тікетів за призначеними користувачами.
Args:
limit (int): Обмеження на кількість користувачів для відображення
Returns:
matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки
"""
try:
if 'Assignee' not in self.df.columns:
logger.warning("Колонка 'Assignee' відсутня")
return None
assignee_counts = self.df['Assignee'].value_counts().head(limit)
fig, ax = plt.subplots(figsize=(14, 6))
sns.barplot(x=assignee_counts.index, y=assignee_counts.values, ax=ax)
for i, v in enumerate(assignee_counts.values):
ax.text(i, v + 0.5, str(v), ha='center')
ax.set_title(f'Кількість тікетів за призначеними користувачами (Топ {limit})')
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_timeline(self, date_column='Created', groupby='day', cumulative=False):
"""
Створення часової діаграми тікетів.
Args:
date_column (str): Колонка з датою ('Created' або 'Updated')
groupby (str): Рівень групування ('day', 'week', 'month')
cumulative (bool): Чи показувати кумулятивну суму
Returns:
matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки
"""
try:
if date_column not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df[date_column]):
logger.warning(f"Колонка '{date_column}' відсутня або не містить дати")
return None
date_col = f"{date_column}_Date" if f"{date_column}_Date" in self.df.columns else date_column
if f"{date_column}_Date" not in self.df.columns:
self.df[f"{date_column}_Date"] = self.df[date_column].dt.date
date_col = f"{date_column}_Date"
if groupby == 'week':
grouped = self.df[date_column].dt.to_period('W').value_counts().sort_index()
title_period = 'тижнями'
elif groupby == 'month':
grouped = self.df[date_column].dt.to_period('M').value_counts().sort_index()
title_period = 'місяцями'
else:
grouped = self.df[date_col].value_counts().sort_index()
title_period = 'датами'
if cumulative:
grouped = grouped.cumsum()
title_prefix = 'Загальна кількість'
else:
title_prefix = 'Кількість'
fig, ax = plt.subplots(figsize=(14, 6))
grouped.plot(kind='line', marker='o', ax=ax)
ax.set_title(f'{title_prefix} {date_column.lower()}них тікетів за {title_period}')
ax.set_xlabel('Період')
ax.set_ylabel('Кількість')
ax.grid(True)
plt.tight_layout()
logger.info(f"Часова діаграма для {date_column} успішно створена")
return fig
except Exception as e:
logger.error(f"Помилка при створенні часової діаграми: {e}")
return None
def plot_heatmap(self, row_col='Issue Type', column_col='Status'):
"""
Створення теплової карти для візуалізації взаємозв'язку між двома категоріями.
Args:
row_col (str): Назва колонки для рядків (наприклад, 'Issue Type')
column_col (str): Назва колонки для стовпців (наприклад, 'Status')
Returns:
matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки
"""
try:
if row_col not in self.df.columns or column_col not in self.df.columns:
logger.warning(f"Колонки '{row_col}' або '{column_col}' відсутні в даних")
return None
pivot_table = pd.crosstab(self.df[row_col], self.df[column_col])
fig, ax = plt.subplots(figsize=(14, 8))
sns.heatmap(pivot_table, annot=True, fmt='d', cmap='YlGnBu', ax=ax)
ax.set_title(f'Розподіл тікетів: {row_col} за {column_col}')
plt.tight_layout()
logger.info(f"Теплова карта для {row_col} за {column_col} успішно створена")
return fig
except Exception as e:
logger.error(f"Помилка при створенні теплової карти: {e}")
return None
def plot_project_timeline(self):
"""
Створення часової шкали проекту, що показує зміну статусів з часом.
Returns:
tuple: (fig1, fig2) - об'єкти figure для різних візуалізацій або (None, 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, None
if 'Updated' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Updated']):
logger.warning("Колонка 'Updated' відсутня або не містить дати")
return None, None
if 'Status' not in self.df.columns:
logger.warning("Колонка 'Status' відсутня")
return None, 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
})
timeline_df = pd.DataFrame(timeline_data)
timeline_df['Date'] = pd.to_datetime(timeline_df['Date'])
fig1, ax1 = plt.subplots(figsize=(16, 8))
ax1.plot(timeline_df['Date'], timeline_df['Total'], marker='o', linewidth=2, label='Загальна кількість')
status_columns = [col for col in timeline_df.columns if col not in ['Date', 'Total']]
for status in status_columns:
ax1.plot(timeline_df['Date'], timeline_df[status], marker='.', linestyle='--', label=status)
ax1.set_title('Зміна стану проекту з часом')
ax1.set_xlabel('Дата')
ax1.set_ylabel('Кількість тікетів')
plt.xticks(rotation=45)
ax1.grid(True)
ax1.legend()
plt.tight_layout()
fig2, ax2 = plt.subplots(figsize=(16, 8))
status_data = timeline_df[['Date'] + status_columns].set_index('Date')
status_data.plot.area(ax=ax2, stacked=True, alpha=0.7)
ax2.set_title('Склад тікетів за статусами')
ax2.set_xlabel('Дата')
ax2.set_ylabel('Кількість тікетів')
ax2.grid(True)
plt.tight_layout()
logger.info("Часова шкала проекту успішно створена")
return fig1, fig2
except Exception as e:
logger.error(f"Помилка при створенні часової шкали проекту: {e}")
return None, None
def generate_infographic(self):
"""
Генерація комплексної інфографіки з ключовими показниками
Returns:
matplotlib.figure.Figure: Об'єкт figure з інфографікою
"""
try:
fig = plt.figure(figsize=(20, 15))
fig.suptitle('Зведений аналіз проекту в Jira', fontsize=24)
ax1 = fig.add_subplot(2, 2, 1)
if 'Status' in self.df.columns:
status_counts = self.df['Status'].value_counts()
sns.barplot(x=status_counts.index, y=status_counts.values, ax=ax1)
ax1.set_title('Розподіл за статусами')
ax1.set_xlabel('Статус')
ax1.set_ylabel('Кількість')
ax1.tick_params(axis='x', rotation=45)
ax2 = fig.add_subplot(2, 2, 2)
if 'Priority' in self.df.columns:
priority_counts = self.df['Priority'].value_counts()
try:
priority_order = ['Highest', 'High', 'Medium', 'Low', 'Lowest']
priority_counts = priority_counts.reindex(priority_order, fill_value=0)
except Exception as ex:
logger.warning(f"Не вдалося впорядкувати пріоритети: {ex}")
colors = ['#FF5555', '#FF9C5A', '#FFCC5A', '#5AFF96', '#5AC8FF']
sns.barplot(x=priority_counts.index, y=priority_counts.values, ax=ax2, palette=colors[:len(priority_counts)])
ax2.set_title('Розподіл за пріоритетами')
ax2.set_xlabel('Пріоритет')
ax2.set_ylabel('Кількість')
ax2.tick_params(axis='x', rotation=45)
ax3 = fig.add_subplot(2, 2, 3)
if 'Created' in self.df.columns and pd.api.types.is_datetime64_dtype(self.df['Created']):
created_dates = self.df['Created'].dt.date.value_counts().sort_index()
created_cumulative = created_dates.cumsum()
created_cumulative.plot(ax=ax3, marker='o')
ax3.set_title('Кумулятивне створення тікетів')
ax3.set_xlabel('Дата')
ax3.set_ylabel('Кількість')
ax3.grid(True)
ax4 = fig.add_subplot(2, 2, 4)
if 'Status' in self.df.columns and 'Issue Type' in self.df.columns:
pivot_table = pd.crosstab(self.df['Issue Type'], self.df['Status'])
sns.heatmap(pivot_table, annot=True, fmt='d', cmap='YlGnBu', ax=ax4)
ax4.set_title('Розподіл: Типи за Статусами')
ax4.tick_params(axis='x', rotation=45)
plt.tight_layout(rect=[0, 0, 1, 0.96])
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['assignee'] = self.plot_assignee_counts(limit=10)
plots['created_timeline'] = self.plot_timeline(date_column='Created', groupby='day')
plots['updated_timeline'] = self.plot_timeline(date_column='Updated', groupby='day')
plots['created_cumulative'] = self.plot_timeline(date_column='Created', cumulative=True)
plots['inactive'] = self.plot_inactive_issues()
plots['heatmap_type_status'] = self.plot_heatmap(row_col='Issue Type', column_col='Status')
timeline_plots = self.plot_project_timeline()
if timeline_plots[0] is not None:
plots['project_timeline'] = timeline_plots[0]
plots['project_composition'] = timeline_plots[1]
if output_dir:
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