Spaces:
Runtime error
Runtime error
Покращена візуалізація рапортів
Browse files- .gitignore +1 -0
- app.py +139 -0
- interface.py +252 -0
- 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 |
-
|
60 |
|
61 |
-
|
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 |
-
|
106 |
|
107 |
-
# Кольори для різних пріоритетів
|
108 |
colors = ['#FF5555', '#FF9C5A', '#FFCC5A', '#5AFF96', '#5AC8FF']
|
109 |
if len(priority_counts) <= len(colors):
|
110 |
-
|
111 |
else:
|
112 |
-
|
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 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
430 |
plots['inactive'] = self.plot_inactive_issues()
|
431 |
-
plots['
|
432 |
-
|
|
|
|
|
|
|
|
|
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
|