DocUA commited on
Commit
fb9ceee
·
1 Parent(s): 26e822e

Покращена візуалізація рапортів

Browse files
Files changed (4) hide show
  1. .gitignore +1 -0
  2. app.py +139 -0
  3. interface.py +252 -0
  4. modules/data_analysis/visualizations.py +280 -90
.gitignore CHANGED
@@ -10,3 +10,4 @@ __pycache__/
10
  *.log
11
  venv_new/
12
  temp/
 
 
10
  *.log
11
  venv_new/
12
  temp/
13
+ reports/
app.py CHANGED
@@ -180,6 +180,145 @@ class JiraAssistantApp:
180
  from modules.data_import.jira_api import JiraConnector
181
  return JiraConnector.test_connection(jira_url, username, api_token)
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  # Створення екземпляру додатку
184
  app = JiraAssistantApp()
185
 
 
180
  from modules.data_import.jira_api import JiraConnector
181
  return JiraConnector.test_connection(jira_url, username, api_token)
182
 
183
+ def generate_visualization(self, viz_type, limit=10, groupby="day"):
184
+ """
185
+ Генерація конкретної візуалізації
186
+
187
+ Args:
188
+ viz_type (str): Тип візуалізації
189
+ limit (int): Ліміт для топ-N елементів
190
+ groupby (str): Групування для часових діаграм ('day', 'week', 'month')
191
+
192
+ Returns:
193
+ matplotlib.figure.Figure: Об'єкт figure
194
+ """
195
+ if self.current_data is None:
196
+ logger.error("Немає даних для візуалізації")
197
+ return None
198
+
199
+ # Створюємо візуалізатор
200
+ visualizer = JiraVisualizer(self.current_data)
201
+
202
+ # Вибір типу візуалізації
203
+ if viz_type == "Статуси":
204
+ return visualizer.plot_status_counts()
205
+ elif viz_type == "Пріоритети":
206
+ return visualizer.plot_priority_counts()
207
+ elif viz_type == "Типи тікетів":
208
+ return visualizer.plot_type_counts()
209
+ elif viz_type == "Призначені користувачі":
210
+ return visualizer.plot_assignee_counts(limit=limit)
211
+ elif viz_type == "Активність створення":
212
+ return visualizer.plot_timeline(date_column='Created', groupby=groupby, cumulative=False)
213
+ elif viz_type == "Активність оновлення":
214
+ return visualizer.plot_timeline(date_column='Updated', groupby=groupby, cumulative=False)
215
+ elif viz_type == "Кумулятивне створення":
216
+ return visualizer.plot_timeline(date_column='Created', groupby=groupby, cumulative=True)
217
+ elif viz_type == "Неактивні тікети":
218
+ return visualizer.plot_inactive_issues()
219
+ elif viz_type == "Теплова карта: Типи/Статуси":
220
+ return visualizer.plot_heatmap(row_col='Issue Type', column_col='Status')
221
+ elif viz_type == "Часова шкала проекту":
222
+ timeline_plots = visualizer.plot_project_timeline()
223
+ return timeline_plots[0] if timeline_plots[0] is not None else None
224
+ elif viz_type == "Склад статусів з часом":
225
+ timeline_plots = visualizer.plot_project_timeline()
226
+ return timeline_plots[1] if timeline_plots[1] is not None else None
227
+ else:
228
+ logger.error(f"Невідомий тип візуалізації: {viz_type}")
229
+ return None
230
+
231
+ def generate_infographic(self):
232
+ """
233
+ Генерація комплексної інфографіки з ключовими показниками
234
+
235
+ Returns:
236
+ matplotlib.figure.Figure: Об'єкт figure з інфографікою
237
+ """
238
+ try:
239
+ if self.current_data is None:
240
+ logger.error("Немає даних для інфографіки")
241
+ return None
242
+
243
+ # Створюємо візуалізатор
244
+ visualizer = JiraVisualizer(self.current_data)
245
+
246
+ # Генеруємо інфографіку
247
+ return visualizer.generate_infographic()
248
+
249
+ except Exception as e:
250
+ logger.error(f"Помилка при генерації інфографіки: {e}")
251
+ return None
252
+
253
+ def save_visualization(self, viz_type, filepath, limit=10, groupby="day"):
254
+ """
255
+ Збереження конкретної візуалізації у файл
256
+
257
+ Args:
258
+ viz_type (str): Тип візуалізації
259
+ filepath (str): Шлях для збереження файлу
260
+ limit (int): Ліміт для топ-N елементів
261
+ groupby (str): Групування для часових діаграм
262
+
263
+ Returns:
264
+ str: Шлях до збереженого файлу або повідомлення про помилку
265
+ """
266
+ try:
267
+ fig = self.generate_visualization(viz_type, limit, groupby)
268
+
269
+ if fig is None:
270
+ return f"Помилка: не вдалося створити візуалізацію типу '{viz_type}'"
271
+
272
+ # Перевірка наявності розширення
273
+ if not any(filepath.lower().endswith(ext) for ext in ['.png', '.jpg', '.svg', '.pdf']):
274
+ filepath += '.png'
275
+
276
+ # Створення директорії, якщо не існує
277
+ os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
278
+
279
+ # Збереження візуалізації
280
+ fig.savefig(filepath, dpi=300, bbox_inches='tight')
281
+
282
+ return f"Візуалізацію успішно збережено: {filepath}"
283
+
284
+ except Exception as e:
285
+ error_msg = f"Помилка при збереженні візуалізації: {str(e)}\n\n{traceback.format_exc()}"
286
+ logger.error(error_msg)
287
+ return error_msg
288
+
289
+ def save_infographic(self, filepath):
290
+ """
291
+ Збереження інфографіки у файл
292
+
293
+ Args:
294
+ filepath (str): Шлях для збереження файлу
295
+
296
+ Returns:
297
+ str: Шлях до збереженого файлу або повідомлення про помилку
298
+ """
299
+ try:
300
+ infographic = self.generate_infographic()
301
+
302
+ if infographic is None:
303
+ return "Помилка: не вдалося створити інфографіку"
304
+
305
+ # Перевірка наявності розширення
306
+ if not any(filepath.lower().endswith(ext) for ext in ['.png', '.jpg', '.svg', '.pdf']):
307
+ filepath += '.png'
308
+
309
+ # Створення директорії, якщо не існує
310
+ os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
311
+
312
+ # Збереження інфографіки
313
+ infographic.savefig(filepath, dpi=300, bbox_inches='tight')
314
+
315
+ return f"Інфографіку успішно збережено: {filepath}"
316
+
317
+ except Exception as e:
318
+ error_msg = f"Помилка при збереженні інфографіки: {str(e)}\n\n{traceback.format_exc()}"
319
+ logger.error(error_msg)
320
+ return error_msg
321
+
322
  # Створення екземпляру додатку
323
  app = JiraAssistantApp()
324
 
interface.py CHANGED
@@ -91,6 +91,116 @@ def launch_interface(app):
91
  else:
92
  return "❌ Помилка підключення до Jira. Перевірте введені дані."
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  # Створення інтерфейсу Gradio
95
  with gr.Blocks(title="Jira AI Assistant") as interface:
96
  gr.Markdown("# 🔍 Jira AI Assistant")
@@ -148,6 +258,91 @@ def launch_interface(app):
148
  outputs=[save_output]
149
  )
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  with gr.Tab("Jira API"):
152
  gr.Markdown("## Підключення до Jira API")
153
 
@@ -203,6 +398,63 @@ def launch_interface(app):
203
  lines=3
204
  )
205
  slack_send_btn = gr.Button("Надіслати у Slack", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
  # Запуск інтерфейсу
208
  interface.launch()
 
91
  else:
92
  return "❌ Помилка підключення до Jira. Перевірте введені дані."
93
 
94
+ # Функція для обробки запиту візуалізації
95
+ def on_viz_generate_clicked(viz_type, limit, groupby_text):
96
+ # Конвертація групування в формат API
97
+ groupby_map = {"день": "day", "тиждень": "week", "місяць": "month"}
98
+ groupby = groupby_map.get(groupby_text, "day")
99
+
100
+ # Якщо немає проаналізованих даних
101
+ if not hasattr(app, 'current_data') or app.current_data is None:
102
+ return gr.Plot.update(value=None), "Спочатку завантажте та проаналізуйте дані"
103
+
104
+ # Генерація візуалізації
105
+ fig = app.generate_visualization(viz_type, limit=limit, groupby=groupby)
106
+
107
+ if fig:
108
+ return fig, None
109
+ else:
110
+ return None, f"Не вдалося згенерувати візуалізацію типу '{viz_type}'"
111
+
112
+ # Функція для збереження конкретної візуалізації
113
+ def save_visualization(viz_type, limit, groupby_text, filename):
114
+ try:
115
+ # Конвертація групування в формат API
116
+ groupby_map = {"день": "day", "тиждень": "week", "місяць": "month"}
117
+ groupby = groupby_map.get(groupby_text, "day")
118
+
119
+ # Генерація візуалізації для збереження
120
+ fig = app.generate_visualization(viz_type, limit=limit, groupby=groupby)
121
+
122
+ if fig is None:
123
+ return "Помилка: не вдалося створити візуалізацію"
124
+
125
+ # Створення імені файлу, якщо не вказано
126
+ if not filename:
127
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
128
+ viz_type_clean = viz_type.lower().replace(' ', '_').replace(':', '_')
129
+ filename = f"viz_{viz_type_clean}_{timestamp}.png"
130
+
131
+ # Перевірка наявності розширення
132
+ if not any(filename.lower().endswith(ext) for ext in ['.png', '.jpg', '.svg', '.pdf']):
133
+ filename += '.png'
134
+
135
+ # Створення директорії, якщо не існує
136
+ reports_dir = Path("reports/visualizations")
137
+ reports_dir.mkdir(parents=True, exist_ok=True)
138
+
139
+ # Шлях до файлу
140
+ filepath = reports_dir / filename
141
+
142
+ # Збереження візуалізації
143
+ fig.savefig(filepath, dpi=300, bbox_inches='tight')
144
+ plt.close(fig)
145
+
146
+ return f"✅ Візуалізацію збережено: {filepath}"
147
+ except Exception as e:
148
+ import traceback
149
+ error_msg = f"Помилка збереження візуалізації: {str(e)}\n\n{traceback.format_exc()}"
150
+ logger.error(error_msg)
151
+ return error_msg
152
+
153
+ # Функція для генерації інфографіки
154
+ def generate_infographic():
155
+ if not hasattr(app, 'current_data') or app.current_data is None:
156
+ return None, "Спочатку завантажте та проаналізуйте дані"
157
+
158
+ infographic = app.generate_infographic()
159
+
160
+ if infographic is not None:
161
+ return infographic, "Інфографіку успішно створено"
162
+ else:
163
+ return None, "Помилка: не вдалося створити інфографіку"
164
+
165
+ # Функція для збереження інфографіки
166
+ def save_infographic(filename):
167
+ try:
168
+ if not hasattr(app, 'current_data') or app.current_data is None:
169
+ return "Помилка: спочатку завантажте та проаналізуйте дані"
170
+
171
+ # Генерація інфографіки
172
+ infographic = app.generate_infographic()
173
+
174
+ if infographic is None:
175
+ return "Помилка: не вдалося створити інфографіку"
176
+
177
+ # Створення імені файлу, якщо не вказано
178
+ if not filename:
179
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
180
+ filename = f"jira_infographic_{timestamp}.png"
181
+
182
+ # Перевірка наявності розширення
183
+ if not any(filename.lower().endswith(ext) for ext in ['.png', '.jpg', '.svg', '.pdf']):
184
+ filename += '.png'
185
+
186
+ # Створення директорії, якщо не існує
187
+ reports_dir = Path("reports/infographics")
188
+ reports_dir.mkdir(parents=True, exist_ok=True)
189
+
190
+ # Шлях до файлу
191
+ filepath = reports_dir / filename
192
+
193
+ # Збереження інфографіки
194
+ infographic.savefig(filepath, dpi=300, bbox_inches='tight')
195
+ plt.close(infographic)
196
+
197
+ return f"✅ Інфографіку збережено: {filepath}"
198
+ except Exception as e:
199
+ import traceback
200
+ error_msg = f"Помилка збереження інфографіки: {str(e)}\n\n{traceback.format_exc()}"
201
+ logger.error(error_msg)
202
+ return error_msg
203
+
204
  # Створення інтерфейсу Gradio
205
  with gr.Blocks(title="Jira AI Assistant") as interface:
206
  gr.Markdown("# 🔍 Jira AI Assistant")
 
258
  outputs=[save_output]
259
  )
260
 
261
+ # Нова вкладка для розширених візуалізацій
262
+ with gr.Tab("Візуалізації"):
263
+ gr.Markdown("## Типи візуалізацій")
264
+
265
+ with gr.Row():
266
+ viz_type = gr.Dropdown(
267
+ choices=[
268
+ "Статуси", "Пріоритети", "Типи тікетів", "Призначені користувачі",
269
+ "Активність створення", "Активність оновлення", "Кумулятивне створення",
270
+ "Неактивні тікети", "Теплова карта: Типи/Статуси", "Часова шкала проекту", "Склад статусів з часом"
271
+ ],
272
+ value="Статуси",
273
+ label="Тип візуалізації"
274
+ )
275
+ viz_generate_btn = gr.Button("Генерувати", variant="primary")
276
+
277
+ # Додаткові параметри для візуалізацій
278
+ with gr.Accordion("Параметри візуалізації", open=False):
279
+ with gr.Row():
280
+ viz_param_limit = gr.Slider(minimum=5, maximum=20, value=10, step=1,
281
+ label="Ліміт для топ-візуалізацій")
282
+ viz_param_groupby = gr.Dropdown(
283
+ choices=["день", "тиждень", "місяць"],
284
+ value="день",
285
+ label="Групування для часових діаграм"
286
+ )
287
+
288
+ with gr.Row():
289
+ viz_plot = gr.Plot(label="Візуалізація")
290
+ viz_status = gr.Textbox(label="Статус", visible=False)
291
+
292
+ # Секція збереження візуалізації
293
+ with gr.Row():
294
+ viz_filename = gr.Textbox(
295
+ label="Ім'я файлу (опціонально)",
296
+ placeholder="Залиште порожнім для автоматичного імені"
297
+ )
298
+ viz_save_btn = gr.Button("Зберегти візуалізацію", variant="secondary")
299
+ viz_save_status = gr.Textbox(label="Статус збереження")
300
+
301
+ # Прив'язуємо обробники подій для візуалізацій
302
+ viz_generate_btn.click(
303
+ on_viz_generate_clicked,
304
+ inputs=[viz_type, viz_param_limit, viz_param_groupby],
305
+ outputs=[viz_plot, viz_status]
306
+ )
307
+
308
+ viz_save_btn.click(
309
+ save_visualization,
310
+ inputs=[viz_type, viz_param_limit, viz_param_groupby, viz_filename],
311
+ outputs=[viz_save_status]
312
+ )
313
+
314
+ # Вкладка для інфографіки
315
+ with gr.Tab("Інфографіка"):
316
+ gr.Markdown("## Комплексна інфографіка")
317
+ gr.Markdown("Створює зведену інфографіку з ключовими показниками проекту на основі проаналізованих даних.")
318
+
319
+ with gr.Row():
320
+ infographic_generate_btn = gr.Button("Створити інфографіку", variant="primary")
321
+
322
+ with gr.Row():
323
+ infographic_plot = gr.Plot(label="Зведена інфографіка")
324
+ infographic_status = gr.Textbox(label="Статус")
325
+
326
+ with gr.Row():
327
+ infographic_filename = gr.Textbox(
328
+ label="Ім'я файлу (опціонально)",
329
+ placeholder="Залиште порожнім для автоматичного імені"
330
+ )
331
+ infographic_save_btn = gr.Button("Зберегти інфографіку", variant="secondary")
332
+
333
+ # Прив'язка обробників для інфографіки
334
+ infographic_generate_btn.click(
335
+ generate_infographic,
336
+ inputs=[],
337
+ outputs=[infographic_plot, infographic_status]
338
+ )
339
+
340
+ infographic_save_btn.click(
341
+ save_infographic,
342
+ inputs=[infographic_filename],
343
+ outputs=[infographic_status]
344
+ )
345
+
346
  with gr.Tab("Jira API"):
347
  gr.Markdown("## Підключення до Jira API")
348
 
 
398
  lines=3
399
  )
400
  slack_send_btn = gr.Button("Надіслати у Slack", interactive=False)
401
+
402
+ with gr.Tab("Налаштування"):
403
+ gr.Markdown("## Налаштування програми")
404
+
405
+ with gr.Accordion("Загальні налаштування", open=True):
406
+ with gr.Row():
407
+ theme_dropdown = gr.Dropdown(
408
+ choices=["Світла", "Темна", "Системна"],
409
+ value="Системна",
410
+ label="Тема інтерфейсу"
411
+ )
412
+ language_dropdown = gr.Dropdown(
413
+ choices=["Українська", "English"],
414
+ value="Українська",
415
+ label="Мова інтерфейсу"
416
+ )
417
+
418
+ chart_style = gr.Dropdown(
419
+ choices=["ggplot", "seaborn", "bmh", "classic", "dark_background"],
420
+ value="ggplot",
421
+ label="Стиль графіків"
422
+ )
423
+
424
+ with gr.Accordion("Налаштування AI", open=True):
425
+ with gr.Row():
426
+ openai_api_key = gr.Textbox(
427
+ label="OpenAI API ключ",
428
+ placeholder="sk-...",
429
+ type="password"
430
+ )
431
+ openai_model = gr.Dropdown(
432
+ choices=["gpt-3.5-turbo", "gpt-4", "gpt-4o", "gpt-4o-mini"],
433
+ value="gpt-3.5-turbo",
434
+ label="Модель OpenAI"
435
+ )
436
+
437
+ with gr.Row():
438
+ gemini_api_key = gr.Textbox(
439
+ label="Google Gemini API ключ",
440
+ placeholder="...",
441
+ type="password"
442
+ )
443
+ gemini_model = gr.Dropdown(
444
+ choices=["gemini-pro", "gemini-1.5-pro"],
445
+ value="gemini-pro",
446
+ label="Модель Gemini"
447
+ )
448
+
449
+ save_settings_btn = gr.Button("Зберегти налаштування", variant="primary")
450
+ settings_status = gr.Textbox(label="Статус")
451
+
452
+ # Заглушка для функціоналу налаштувань
453
+ save_settings_btn.click(
454
+ lambda: "Налаштування збережено. Зміни набудуть чинності після перезапуску програми.",
455
+ inputs=[],
456
+ outputs=[settings_status]
457
+ )
458
 
459
  # Запуск інтерфейсу
460
  interface.launch()
modules/data_analysis/visualizations.py CHANGED
@@ -7,6 +7,7 @@ import logging
7
 
8
  logger = logging.getLogger(__name__)
9
 
 
10
  class JiraVisualizer:
11
  """
12
  Клас для створення візуалізацій даних Jira
@@ -27,7 +28,6 @@ class JiraVisualizer:
27
  """
28
  plt.style.use('ggplot')
29
  sns.set(style="whitegrid")
30
-
31
  # Налаштування для українських символів
32
  plt.rcParams['font.family'] = 'DejaVu Sans'
33
 
@@ -44,8 +44,6 @@ class JiraVisualizer:
44
  return None
45
 
46
  status_counts = self.df['Status'].value_counts()
47
-
48
- # Створення діаграми
49
  fig, ax = plt.subplots(figsize=(10, 6))
50
 
51
  # Спроба впорядкувати статуси логічно
@@ -55,12 +53,10 @@ class JiraVisualizer:
55
  other_statuses = [s for s in status_counts.index if s not in status_order]
56
  ordered_statuses = available_statuses + other_statuses
57
  status_counts = status_counts.reindex(ordered_statuses)
58
- except:
59
- pass
60
 
61
- bars = sns.barplot(x=status_counts.index, y=status_counts.values, ax=ax)
62
-
63
- # Додаємо підписи значень над стовпцями
64
  for i, v in enumerate(status_counts.values):
65
  ax.text(i, v + 0.5, str(v), ha='center')
66
 
@@ -76,7 +72,7 @@ class JiraVisualizer:
76
  except Exception as e:
77
  logger.error(f"Помилка при створенні діаграми статусів: {e}")
78
  return None
79
-
80
  def plot_priority_counts(self):
81
  """
82
  Створення діаграми розподілу тікетів за пріоритетами.
@@ -90,8 +86,6 @@ class JiraVisualizer:
90
  return None
91
 
92
  priority_counts = self.df['Priority'].value_counts()
93
-
94
- # Створення діаграми
95
  fig, ax = plt.subplots(figsize=(10, 6))
96
 
97
  # Спроба впорядкувати пріоритети логічно
@@ -101,17 +95,15 @@ class JiraVisualizer:
101
  other_priorities = [p for p in priority_counts.index if p not in priority_order]
102
  ordered_priorities = available_priorities + other_priorities
103
  priority_counts = priority_counts.reindex(ordered_priorities)
104
- except:
105
- pass
106
 
107
- # Кольори для різних пріоритетів
108
  colors = ['#FF5555', '#FF9C5A', '#FFCC5A', '#5AFF96', '#5AC8FF']
109
  if len(priority_counts) <= len(colors):
110
- bars = sns.barplot(x=priority_counts.index, y=priority_counts.values, ax=ax, palette=colors[:len(priority_counts)])
111
  else:
112
- bars = sns.barplot(x=priority_counts.index, y=priority_counts.values, ax=ax)
113
 
114
- # Додаємо підписи значень над стовпцями
115
  for i, v in enumerate(priority_counts.values):
116
  ax.text(i, v + 0.5, str(v), ha='center')
117
 
@@ -127,7 +119,7 @@ class JiraVisualizer:
127
  except Exception as e:
128
  logger.error(f"Помилка при створенні діаграми пріоритетів: {e}")
129
  return None
130
-
131
  def plot_type_counts(self):
132
  """
133
  Створення діаграми розподілу тікетів за типами.
@@ -141,13 +133,9 @@ class JiraVisualizer:
141
  return None
142
 
143
  type_counts = self.df['Issue Type'].value_counts()
144
-
145
- # Створення діаграми
146
  fig, ax = plt.subplots(figsize=(10, 6))
 
147
 
148
- bars = sns.barplot(x=type_counts.index, y=type_counts.values, ax=ax)
149
-
150
- # Додаємо підписи значень над стовпцями
151
  for i, v in enumerate(type_counts.values):
152
  ax.text(i, v + 0.5, str(v), ha='center')
153
 
@@ -163,7 +151,7 @@ class JiraVisualizer:
163
  except Exception as e:
164
  logger.error(f"Помилка при створенні діаграми типів: {e}")
165
  return None
166
-
167
  def plot_created_timeline(self):
168
  """
169
  Створення часової діаграми створення тікетів.
@@ -176,16 +164,11 @@ class JiraVisualizer:
176
  logger.warning("Колонка 'Created' відсутня або не містить дат")
177
  return None
178
 
179
- # Додаємо колонку з датою створення (без часу)
180
  if 'Created_Date' not in self.df.columns:
181
  self.df['Created_Date'] = self.df['Created'].dt.date
182
 
183
- # Кількість створених тікетів за датами
184
  created_by_date = self.df['Created_Date'].value_counts().sort_index()
185
-
186
- # Створення діаграми
187
  fig, ax = plt.subplots(figsize=(12, 6))
188
-
189
  created_by_date.plot(kind='line', marker='o', ax=ax)
190
 
191
  ax.set_title('Кількість створених тікетів за датами')
@@ -200,7 +183,7 @@ class JiraVisualizer:
200
  except Exception as e:
201
  logger.error(f"Помилка при створенні часової діаграми: {e}")
202
  return None
203
-
204
  def plot_inactive_issues(self, days=14):
205
  """
206
  Створення діаграми неактивних тікетів.
@@ -216,7 +199,6 @@ class JiraVisualizer:
216
  logger.warning("Колонка 'Updated' відсутня або не містить дат")
217
  return None
218
 
219
- # Визначення неактивних тікетів
220
  cutoff_date = datetime.now() - timedelta(days=days)
221
  inactive_issues = self.df[self.df['Updated'] < cutoff_date]
222
 
@@ -224,16 +206,10 @@ class JiraVisualizer:
224
  logger.warning("Немає неактивних тікетів для візуалізації")
225
  return None
226
 
227
- # Розподіл неактивних тікетів за статусами
228
  if 'Status' in inactive_issues.columns:
229
  inactive_by_status = inactive_issues['Status'].value_counts()
230
-
231
- # Створення діаграми
232
  fig, ax = plt.subplots(figsize=(10, 6))
233
-
234
- bars = sns.barplot(x=inactive_by_status.index, y=inactive_by_status.values, ax=ax)
235
-
236
- # Додаємо підписи значень над стовпцями
237
  for i, v in enumerate(inactive_by_status.values):
238
  ax.text(i, v + 0.5, str(v), ha='center')
239
 
@@ -252,7 +228,7 @@ class JiraVisualizer:
252
  except Exception as e:
253
  logger.error(f"Помилка при створенні діаграми неактивних тікетів: {e}")
254
  return None
255
-
256
  def plot_status_timeline(self, timeline_df=None):
257
  """
258
  Створення діаграми зміни статусів з часом.
@@ -273,60 +249,37 @@ class JiraVisualizer:
273
  logger.warning("Колонка 'Updated' відсутня або не містить дат")
274
  return None
275
 
276
- # Визначення часового діапазону
277
  min_date = self.df['Created'].min().date()
278
  max_date = self.df['Updated'].max().date()
279
-
280
- # Створення часового ряду для кожного дня
281
  date_range = pd.date_range(start=min_date, end=max_date, freq='D')
282
-
283
- # Збір статистики для кожної дати
284
  timeline_data = []
285
 
286
  for date in date_range:
287
  date_str = date.strftime('%Y-%m-%d')
288
-
289
- # Тікети, створені до цієї дати
290
  created_until = self.df[self.df['Created'].dt.date <= date.date()]
291
-
292
- # Статуси тікетів на цю дату
293
  status_counts = {}
294
-
295
- # Для кожного тікета визначаємо його статус на цю дату
296
  for _, row in created_until.iterrows():
297
- # Якщо тікет був оновлений після цієї дати, використовуємо його поточний статус
298
  if row['Updated'].date() >= date.date():
299
  status = row.get('Status', 'Unknown')
300
  status_counts[status] = status_counts.get(status, 0) + 1
301
-
302
- # Додаємо запис для цієї дати
303
  timeline_data.append({
304
  'Date': date_str,
305
  'Total': len(created_until),
306
  **status_counts
307
  })
308
 
309
- # Створення DataFrame
310
  timeline_df = pd.DataFrame(timeline_data)
311
-
312
- # Конвертація Date до datetime
313
  timeline_df['Date'] = pd.to_datetime(timeline_df['Date'])
314
  else:
315
- # Конвертація Date до datetime, якщо потрібно
316
  if not pd.api.types.is_datetime64_dtype(timeline_df['Date']):
317
  timeline_df['Date'] = pd.to_datetime(timeline_df['Date'])
318
 
319
- # Отримання статусів (всі колонки, крім Date і Total)
320
  status_columns = [col for col in timeline_df.columns if col not in ['Date', 'Total']]
321
-
322
  if not status_columns:
323
  logger.warning("Немає даних про статуси для візуалізації")
324
  return None
325
 
326
- # Створення діаграми
327
  fig, ax = plt.subplots(figsize=(14, 8))
328
-
329
- # Створення сетплоту для статусів
330
  status_data = timeline_df[['Date'] + status_columns].set_index('Date')
331
  status_data.plot.area(ax=ax, stacked=True, alpha=0.7)
332
 
@@ -342,7 +295,7 @@ class JiraVisualizer:
342
  except Exception as e:
343
  logger.error(f"Помилка при створенні часової діаграми статусів: {e}")
344
  return None
345
-
346
  def plot_lead_time_by_type(self):
347
  """
348
  Створення діаграми часу виконання за типами тікетів.
@@ -363,36 +316,23 @@ class JiraVisualizer:
363
  logger.warning("Колонка 'Issue Type' відсутня")
364
  return None
365
 
366
- # Конвертація колонки Resolved до datetime, якщо потрібно
367
  if not pd.api.types.is_datetime64_dtype(self.df['Resolved']):
368
  self.df['Resolved'] = pd.to_datetime(self.df['Resolved'], errors='coerce')
369
 
370
- # Фільтрація завершених тікетів
371
  completed_issues = self.df.dropna(subset=['Resolved'])
372
-
373
  if len(completed_issues) == 0:
374
  logger.warning("Немає завершених тікетів для аналізу")
375
  return None
376
 
377
- # Обчислення Lead Time (в днях)
378
  completed_issues['Lead_Time_Days'] = (completed_issues['Resolved'] - completed_issues['Created']).dt.days
379
-
380
- # Фільтрація некоректних значень
381
  valid_lead_time = completed_issues[completed_issues['Lead_Time_Days'] >= 0]
382
-
383
  if len(valid_lead_time) == 0:
384
  logger.warning("Немає валідних даних про час виконання")
385
  return None
386
 
387
- # Обчислення середнього часу виконання за типами
388
  lead_time_by_type = valid_lead_time.groupby('Issue Type')['Lead_Time_Days'].mean()
389
-
390
- # Створення діаграми
391
  fig, ax = plt.subplots(figsize=(10, 6))
392
-
393
- bars = sns.barplot(x=lead_time_by_type.index, y=lead_time_by_type.values, ax=ax)
394
-
395
- # Додаємо підписи значень над стовпцями
396
  for i, v in enumerate(lead_time_by_type.values):
397
  ax.text(i, v + 0.5, f"{v:.1f}", ha='center')
398
 
@@ -408,43 +348,293 @@ class JiraVisualizer:
408
  except Exception as e:
409
  logger.error(f"Помилка при створенні діаграми часу виконання: {e}")
410
  return None
411
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  def plot_all(self, output_dir=None):
413
  """
414
  Створення та збереження всіх діаграм.
415
 
416
  Args:
417
  output_dir (str): Директорія для збереження діаграм.
418
- Якщо None, діаграми не зберігаються.
419
 
420
  Returns:
421
  dict: Словник з об'єктами figure для всіх діаграм
422
  """
423
  plots = {}
424
 
425
- # Створення діаграм
426
  plots['status'] = self.plot_status_counts()
427
  plots['priority'] = self.plot_priority_counts()
428
  plots['type'] = self.plot_type_counts()
429
- plots['created_timeline'] = self.plot_created_timeline()
 
 
 
 
430
  plots['inactive'] = self.plot_inactive_issues()
431
- plots['status_timeline'] = self.plot_status_timeline()
432
- plots['lead_time'] = self.plot_lead_time_by_type()
 
 
 
 
433
 
434
- # Збереження діаграм, якщо вказана директорія
435
  if output_dir:
436
- import os
437
  from pathlib import Path
438
-
439
- # Створення директорії, якщо вона не існує
440
  output_path = Path(output_dir)
441
  output_path.mkdir(exist_ok=True, parents=True)
442
-
443
- # Збереження кожної діаграми
444
  for name, fig in plots.items():
445
  if fig:
446
  fig_path = output_path / f"{name}.png"
447
  fig.savefig(fig_path, dpi=300)
448
  logger.info(f"Діаграма {name} збережена у {fig_path}")
449
 
450
- return plots
 
7
 
8
  logger = logging.getLogger(__name__)
9
 
10
+
11
  class JiraVisualizer:
12
  """
13
  Клас для створення візуалізацій даних Jira
 
28
  """
29
  plt.style.use('ggplot')
30
  sns.set(style="whitegrid")
 
31
  # Налаштування для українських символів
32
  plt.rcParams['font.family'] = 'DejaVu Sans'
33
 
 
44
  return None
45
 
46
  status_counts = self.df['Status'].value_counts()
 
 
47
  fig, ax = plt.subplots(figsize=(10, 6))
48
 
49
  # Спроба впорядкувати статуси логічно
 
53
  other_statuses = [s for s in status_counts.index if s not in status_order]
54
  ordered_statuses = available_statuses + other_statuses
55
  status_counts = status_counts.reindex(ordered_statuses)
56
+ except Exception as ex:
57
+ logger.warning(f"Не вдалося впорядкувати статуси: {ex}")
58
 
59
+ sns.barplot(x=status_counts.index, y=status_counts.values, ax=ax)
 
 
60
  for i, v in enumerate(status_counts.values):
61
  ax.text(i, v + 0.5, str(v), ha='center')
62
 
 
72
  except Exception as e:
73
  logger.error(f"Помилка при створенні діаграми статусів: {e}")
74
  return None
75
+
76
  def plot_priority_counts(self):
77
  """
78
  Створення діаграми розподілу тікетів за пріоритетами.
 
86
  return None
87
 
88
  priority_counts = self.df['Priority'].value_counts()
 
 
89
  fig, ax = plt.subplots(figsize=(10, 6))
90
 
91
  # Спроба впорядкувати пріоритети логічно
 
95
  other_priorities = [p for p in priority_counts.index if p not in priority_order]
96
  ordered_priorities = available_priorities + other_priorities
97
  priority_counts = priority_counts.reindex(ordered_priorities)
98
+ except Exception as ex:
99
+ logger.warning(f"Не вдалося впорядкувати пріоритети: {ex}")
100
 
 
101
  colors = ['#FF5555', '#FF9C5A', '#FFCC5A', '#5AFF96', '#5AC8FF']
102
  if len(priority_counts) <= len(colors):
103
+ sns.barplot(x=priority_counts.index, y=priority_counts.values, ax=ax, palette=colors[:len(priority_counts)])
104
  else:
105
+ sns.barplot(x=priority_counts.index, y=priority_counts.values, ax=ax)
106
 
 
107
  for i, v in enumerate(priority_counts.values):
108
  ax.text(i, v + 0.5, str(v), ha='center')
109
 
 
119
  except Exception as e:
120
  logger.error(f"Помилка при створенні діаграми пріоритетів: {e}")
121
  return None
122
+
123
  def plot_type_counts(self):
124
  """
125
  Створення діаграми розподілу тікетів за типами.
 
133
  return None
134
 
135
  type_counts = self.df['Issue Type'].value_counts()
 
 
136
  fig, ax = plt.subplots(figsize=(10, 6))
137
+ sns.barplot(x=type_counts.index, y=type_counts.values, ax=ax)
138
 
 
 
 
139
  for i, v in enumerate(type_counts.values):
140
  ax.text(i, v + 0.5, str(v), ha='center')
141
 
 
151
  except Exception as e:
152
  logger.error(f"Помилка при створенні діаграми типів: {e}")
153
  return None
154
+
155
  def plot_created_timeline(self):
156
  """
157
  Створення часової діаграми створення тікетів.
 
164
  logger.warning("Колонка 'Created' відсутня або не містить дат")
165
  return None
166
 
 
167
  if 'Created_Date' not in self.df.columns:
168
  self.df['Created_Date'] = self.df['Created'].dt.date
169
 
 
170
  created_by_date = self.df['Created_Date'].value_counts().sort_index()
 
 
171
  fig, ax = plt.subplots(figsize=(12, 6))
 
172
  created_by_date.plot(kind='line', marker='o', ax=ax)
173
 
174
  ax.set_title('Кількість створених тікетів за датами')
 
183
  except Exception as e:
184
  logger.error(f"Помилка при створенні часової діаграми: {e}")
185
  return None
186
+
187
  def plot_inactive_issues(self, days=14):
188
  """
189
  Створення діаграми неактивних тікетів.
 
199
  logger.warning("Колонка 'Updated' відсутня або не містить дат")
200
  return None
201
 
 
202
  cutoff_date = datetime.now() - timedelta(days=days)
203
  inactive_issues = self.df[self.df['Updated'] < cutoff_date]
204
 
 
206
  logger.warning("Немає неактивних тікетів для візуалізації")
207
  return None
208
 
 
209
  if 'Status' in inactive_issues.columns:
210
  inactive_by_status = inactive_issues['Status'].value_counts()
 
 
211
  fig, ax = plt.subplots(figsize=(10, 6))
212
+ sns.barplot(x=inactive_by_status.index, y=inactive_by_status.values, ax=ax)
 
 
 
213
  for i, v in enumerate(inactive_by_status.values):
214
  ax.text(i, v + 0.5, str(v), ha='center')
215
 
 
228
  except Exception as e:
229
  logger.error(f"Помилка при створенні діаграми неактивних тікетів: {e}")
230
  return None
231
+
232
  def plot_status_timeline(self, timeline_df=None):
233
  """
234
  Створення діаграми зміни статусів з часом.
 
249
  logger.warning("Колонка 'Updated' відсутня або не містить дат")
250
  return None
251
 
 
252
  min_date = self.df['Created'].min().date()
253
  max_date = self.df['Updated'].max().date()
 
 
254
  date_range = pd.date_range(start=min_date, end=max_date, freq='D')
 
 
255
  timeline_data = []
256
 
257
  for date in date_range:
258
  date_str = date.strftime('%Y-%m-%d')
 
 
259
  created_until = self.df[self.df['Created'].dt.date <= date.date()]
 
 
260
  status_counts = {}
 
 
261
  for _, row in created_until.iterrows():
 
262
  if row['Updated'].date() >= date.date():
263
  status = row.get('Status', 'Unknown')
264
  status_counts[status] = status_counts.get(status, 0) + 1
 
 
265
  timeline_data.append({
266
  'Date': date_str,
267
  'Total': len(created_until),
268
  **status_counts
269
  })
270
 
 
271
  timeline_df = pd.DataFrame(timeline_data)
 
 
272
  timeline_df['Date'] = pd.to_datetime(timeline_df['Date'])
273
  else:
 
274
  if not pd.api.types.is_datetime64_dtype(timeline_df['Date']):
275
  timeline_df['Date'] = pd.to_datetime(timeline_df['Date'])
276
 
 
277
  status_columns = [col for col in timeline_df.columns if col not in ['Date', 'Total']]
 
278
  if not status_columns:
279
  logger.warning("Немає даних про статуси для візуалізації")
280
  return None
281
 
 
282
  fig, ax = plt.subplots(figsize=(14, 8))
 
 
283
  status_data = timeline_df[['Date'] + status_columns].set_index('Date')
284
  status_data.plot.area(ax=ax, stacked=True, alpha=0.7)
285
 
 
295
  except Exception as e:
296
  logger.error(f"Помилка при створенні часової діаграми статусів: {e}")
297
  return None
298
+
299
  def plot_lead_time_by_type(self):
300
  """
301
  Створення діаграми часу виконання за типами тікетів.
 
316
  logger.warning("Колонка 'Issue Type' відсутня")
317
  return None
318
 
 
319
  if not pd.api.types.is_datetime64_dtype(self.df['Resolved']):
320
  self.df['Resolved'] = pd.to_datetime(self.df['Resolved'], errors='coerce')
321
 
 
322
  completed_issues = self.df.dropna(subset=['Resolved'])
 
323
  if len(completed_issues) == 0:
324
  logger.warning("Немає завершених тікетів для аналізу")
325
  return None
326
 
 
327
  completed_issues['Lead_Time_Days'] = (completed_issues['Resolved'] - completed_issues['Created']).dt.days
 
 
328
  valid_lead_time = completed_issues[completed_issues['Lead_Time_Days'] >= 0]
 
329
  if len(valid_lead_time) == 0:
330
  logger.warning("Немає валідних даних про час виконання")
331
  return None
332
 
 
333
  lead_time_by_type = valid_lead_time.groupby('Issue Type')['Lead_Time_Days'].mean()
 
 
334
  fig, ax = plt.subplots(figsize=(10, 6))
335
+ sns.barplot(x=lead_time_by_type.index, y=lead_time_by_type.values, ax=ax)
 
 
 
336
  for i, v in enumerate(lead_time_by_type.values):
337
  ax.text(i, v + 0.5, f"{v:.1f}", ha='center')
338
 
 
348
  except Exception as e:
349
  logger.error(f"Помилка при створенні діаграми часу виконання: {e}")
350
  return None
351
+
352
+ # Нові методи, додані до класу JiraVisualizer
353
+
354
+ def plot_assignee_counts(self, limit=10):
355
+ """
356
+ Створення діаграми розподілу тікетів за призначеними користувачами.
357
+
358
+ Args:
359
+ limit (int): Обмеження на кількість користувачів для відображення
360
+
361
+ Returns:
362
+ matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки
363
+ """
364
+ try:
365
+ if 'Assignee' not in self.df.columns:
366
+ logger.warning("Колонка 'Assignee' відсутня")
367
+ return None
368
+
369
+ assignee_counts = self.df['Assignee'].value_counts().head(limit)
370
+ fig, ax = plt.subplots(figsize=(14, 6))
371
+ sns.barplot(x=assignee_counts.index, y=assignee_counts.values, ax=ax)
372
+ for i, v in enumerate(assignee_counts.values):
373
+ ax.text(i, v + 0.5, str(v), ha='center')
374
+
375
+ ax.set_title(f'Кількість тікетів за призначеними користувачами (Топ {limit})')
376
+ ax.set_xlabel('Призначений користувач')
377
+ ax.set_ylabel('Кількість')
378
+ plt.xticks(rotation=45)
379
+ plt.tight_layout()
380
+
381
+ logger.info("Діаграма призначених користувачів успішно створена")
382
+ return fig
383
+ except Exception as e:
384
+ logger.error(f"Помилка при створенні діаграми призначених користувачів: {e}")
385
+ return None
386
+
387
+ def plot_timeline(self, date_column='Created', groupby='day', cumulative=False):
388
+ """
389
+ Створення часової діаграми тікетів.
390
+
391
+ Args:
392
+ date_column (str): Колонка з датою ('Created' або 'Updated')
393
+ groupby (str): Рівень групування ('day', 'week', 'month')
394
+ cumulative (bool): Чи показувати кумулятивну суму
395
+
396
+ Returns:
397
+ matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки
398
+ """
399
+ try:
400
+ if date_column not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df[date_column]):
401
+ logger.warning(f"Колонка '{date_column}' відсутня або не містить дати")
402
+ return None
403
+
404
+ date_col = f"{date_column}_Date" if f"{date_column}_Date" in self.df.columns else date_column
405
+ if f"{date_column}_Date" not in self.df.columns:
406
+ self.df[f"{date_column}_Date"] = self.df[date_column].dt.date
407
+ date_col = f"{date_column}_Date"
408
+
409
+ if groupby == 'week':
410
+ grouped = self.df[date_column].dt.to_period('W').value_counts().sort_index()
411
+ title_period = 'тижнями'
412
+ elif groupby == 'month':
413
+ grouped = self.df[date_column].dt.to_period('M').value_counts().sort_index()
414
+ title_period = 'місяцями'
415
+ else:
416
+ grouped = self.df[date_col].value_counts().sort_index()
417
+ title_period = 'датами'
418
+
419
+ if cumulative:
420
+ grouped = grouped.cumsum()
421
+ title_prefix = 'Загальна кількість'
422
+ else:
423
+ title_prefix = 'Кількість'
424
+
425
+ fig, ax = plt.subplots(figsize=(14, 6))
426
+ grouped.plot(kind='line', marker='o', ax=ax)
427
+
428
+ ax.set_title(f'{title_prefix} {date_column.lower()}них тікетів за {title_period}')
429
+ ax.set_xlabel('Період')
430
+ ax.set_ylabel('Кількість')
431
+ ax.grid(True)
432
+ plt.tight_layout()
433
+
434
+ logger.info(f"Часова діаграма для {date_column} успішно створена")
435
+ return fig
436
+ except Exception as e:
437
+ logger.error(f"Помилка при створенні часової діаграми: {e}")
438
+ return None
439
+
440
+ def plot_heatmap(self, row_col='Issue Type', column_col='Status'):
441
+ """
442
+ Створення теплової карти для візуалізації взаємозв'язку між двома категоріями.
443
+
444
+ Args:
445
+ row_col (str): Назва колонки для рядків (наприклад, 'Issue Type')
446
+ column_col (str): Назва колонки для стовпців (наприклад, 'Status')
447
+
448
+ Returns:
449
+ matplotlib.figure.Figure: Об'єкт figure або None у випадку помилки
450
+ """
451
+ try:
452
+ if row_col not in self.df.columns or column_col not in self.df.columns:
453
+ logger.warning(f"Колонки '{row_col}' або '{column_col}' відсутні в даних")
454
+ return None
455
+
456
+ pivot_table = pd.crosstab(self.df[row_col], self.df[column_col])
457
+ fig, ax = plt.subplots(figsize=(14, 8))
458
+ sns.heatmap(pivot_table, annot=True, fmt='d', cmap='YlGnBu', ax=ax)
459
+
460
+ ax.set_title(f'Розподіл тікетів: {row_col} за {column_col}')
461
+ plt.tight_layout()
462
+
463
+ logger.info(f"Теплова карта для {row_col} за {column_col} успішно створена")
464
+ return fig
465
+ except Exception as e:
466
+ logger.error(f"Помилка при створенні теплової карти: {e}")
467
+ return None
468
+
469
+ def plot_project_timeline(self):
470
+ """
471
+ Створення часової шкали проекту, що показує зміну статусів з часом.
472
+
473
+ Returns:
474
+ tuple: (fig1, fig2) - об'єкти figure для різних візуалізацій або (None, None) у випадку помилки
475
+ """
476
+ try:
477
+ if 'Created' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Created']):
478
+ logger.warning("Колонка 'Created' відсутня або не містить дати")
479
+ return None, None
480
+
481
+ if 'Updated' not in self.df.columns or not pd.api.types.is_datetime64_dtype(self.df['Updated']):
482
+ logger.warning("Колонка 'Updated' відсутня або не містить дати")
483
+ return None, None
484
+
485
+ if 'Status' not in self.df.columns:
486
+ logger.warning("Колонка 'Status' відсутня")
487
+ return None, None
488
+
489
+ min_date = self.df['Created'].min().date()
490
+ max_date = self.df['Updated'].max().date()
491
+ date_range = pd.date_range(start=min_date, end=max_date, freq='D')
492
+ timeline_data = []
493
+
494
+ for date in date_range:
495
+ date_str = date.strftime('%Y-%m-%d')
496
+ created_until = self.df[self.df['Created'].dt.date <= date.date()]
497
+ status_counts = {}
498
+ for _, row in created_until.iterrows():
499
+ if row['Updated'].date() >= date.date():
500
+ status = row.get('Status', 'Unknown')
501
+ status_counts[status] = status_counts.get(status, 0) + 1
502
+ timeline_data.append({
503
+ 'Date': date_str,
504
+ 'Total': len(created_until),
505
+ **status_counts
506
+ })
507
+
508
+ timeline_df = pd.DataFrame(timeline_data)
509
+ timeline_df['Date'] = pd.to_datetime(timeline_df['Date'])
510
+
511
+ fig1, ax1 = plt.subplots(figsize=(16, 8))
512
+ ax1.plot(timeline_df['Date'], timeline_df['Total'], marker='o', linewidth=2, label='Загальна кількість')
513
+ status_columns = [col for col in timeline_df.columns if col not in ['Date', 'Total']]
514
+ for status in status_columns:
515
+ ax1.plot(timeline_df['Date'], timeline_df[status], marker='.', linestyle='--', label=status)
516
+
517
+ ax1.set_title('Зміна стану проекту з часом')
518
+ ax1.set_xlabel('Дата')
519
+ ax1.set_ylabel('Кількість тікетів')
520
+ plt.xticks(rotation=45)
521
+ ax1.grid(True)
522
+ ax1.legend()
523
+ plt.tight_layout()
524
+
525
+ fig2, ax2 = plt.subplots(figsize=(16, 8))
526
+ status_data = timeline_df[['Date'] + status_columns].set_index('Date')
527
+ status_data.plot.area(ax=ax2, stacked=True, alpha=0.7)
528
+
529
+ ax2.set_title('Склад тікетів за статусами')
530
+ ax2.set_xlabel('Дата')
531
+ ax2.set_ylabel('Кількість тікетів')
532
+ ax2.grid(True)
533
+ plt.tight_layout()
534
+
535
+ logger.info("Часова шкала проекту успішно створена")
536
+ return fig1, fig2
537
+ except Exception as e:
538
+ logger.error(f"Помилка при створенні часової шкали проекту: {e}")
539
+ return None, None
540
+
541
+ def generate_infographic(self):
542
+ """
543
+ Генерація комплексної інфографіки з ключовими показниками
544
+
545
+ Returns:
546
+ matplotlib.figure.Figure: Об'єкт figure з інфографікою
547
+ """
548
+ try:
549
+ fig = plt.figure(figsize=(20, 15))
550
+ fig.suptitle('Зведений аналіз проекту в Jira', fontsize=24)
551
+
552
+ ax1 = fig.add_subplot(2, 2, 1)
553
+ if 'Status' in self.df.columns:
554
+ status_counts = self.df['Status'].value_counts()
555
+ sns.barplot(x=status_counts.index, y=status_counts.values, ax=ax1)
556
+ ax1.set_title('Розподіл за статусами')
557
+ ax1.set_xlabel('Статус')
558
+ ax1.set_ylabel('Кількість')
559
+ ax1.tick_params(axis='x', rotation=45)
560
+
561
+ ax2 = fig.add_subplot(2, 2, 2)
562
+ if 'Priority' in self.df.columns:
563
+ priority_counts = self.df['Priority'].value_counts()
564
+ try:
565
+ priority_order = ['Highest', 'High', 'Medium', 'Low', 'Lowest']
566
+ priority_counts = priority_counts.reindex(priority_order, fill_value=0)
567
+ except Exception as ex:
568
+ logger.warning(f"Не в��алося впорядкувати пріоритети: {ex}")
569
+ colors = ['#FF5555', '#FF9C5A', '#FFCC5A', '#5AFF96', '#5AC8FF']
570
+ sns.barplot(x=priority_counts.index, y=priority_counts.values, ax=ax2, palette=colors[:len(priority_counts)])
571
+ ax2.set_title('Розподіл за пріоритетами')
572
+ ax2.set_xlabel('Пріоритет')
573
+ ax2.set_ylabel('Кількість')
574
+ ax2.tick_params(axis='x', rotation=45)
575
+
576
+ ax3 = fig.add_subplot(2, 2, 3)
577
+ if 'Created' in self.df.columns and pd.api.types.is_datetime64_dtype(self.df['Created']):
578
+ created_dates = self.df['Created'].dt.date.value_counts().sort_index()
579
+ created_cumulative = created_dates.cumsum()
580
+ created_cumulative.plot(ax=ax3, marker='o')
581
+ ax3.set_title('Кумулятивне створення тікетів')
582
+ ax3.set_xlabel('Дата')
583
+ ax3.set_ylabel('Кількість')
584
+ ax3.grid(True)
585
+
586
+ ax4 = fig.add_subplot(2, 2, 4)
587
+ if 'Status' in self.df.columns and 'Issue Type' in self.df.columns:
588
+ pivot_table = pd.crosstab(self.df['Issue Type'], self.df['Status'])
589
+ sns.heatmap(pivot_table, annot=True, fmt='d', cmap='YlGnBu', ax=ax4)
590
+ ax4.set_title('Розподіл: Типи за Статусами')
591
+ ax4.tick_params(axis='x', rotation=45)
592
+
593
+ plt.tight_layout(rect=[0, 0, 1, 0.96])
594
+
595
+ logger.info("Інфографіка успішно створена")
596
+ return fig
597
+ except Exception as e:
598
+ logger.error(f"Помилка при створенні інфографіки: {e}")
599
+ return None
600
+
601
  def plot_all(self, output_dir=None):
602
  """
603
  Створення та збереження всіх діаграм.
604
 
605
  Args:
606
  output_dir (str): Директорія для збереження діаграм.
607
+ Якщо None, діаграми не зберігаються.
608
 
609
  Returns:
610
  dict: Словник з об'єктами figure для всіх діаграм
611
  """
612
  plots = {}
613
 
 
614
  plots['status'] = self.plot_status_counts()
615
  plots['priority'] = self.plot_priority_counts()
616
  plots['type'] = self.plot_type_counts()
617
+
618
+ plots['assignee'] = self.plot_assignee_counts(limit=10)
619
+ plots['created_timeline'] = self.plot_timeline(date_column='Created', groupby='day')
620
+ plots['updated_timeline'] = self.plot_timeline(date_column='Updated', groupby='day')
621
+ plots['created_cumulative'] = self.plot_timeline(date_column='Created', cumulative=True)
622
  plots['inactive'] = self.plot_inactive_issues()
623
+ plots['heatmap_type_status'] = self.plot_heatmap(row_col='Issue Type', column_col='Status')
624
+
625
+ timeline_plots = self.plot_project_timeline()
626
+ if timeline_plots[0] is not None:
627
+ plots['project_timeline'] = timeline_plots[0]
628
+ plots['project_composition'] = timeline_plots[1]
629
 
 
630
  if output_dir:
 
631
  from pathlib import Path
 
 
632
  output_path = Path(output_dir)
633
  output_path.mkdir(exist_ok=True, parents=True)
 
 
634
  for name, fig in plots.items():
635
  if fig:
636
  fig_path = output_path / f"{name}.png"
637
  fig.savefig(fig_path, dpi=300)
638
  logger.info(f"Діаграма {name} збережена у {fig_path}")
639
 
640
+ return plots