Загрузка данных
# -*- coding: utf-8 -*-
# gui/forecast_window.py
import tkinter as tk
from tkinter import ttk, scrolledtext
from tkcalendar import DateEntry
from modules.forecast_modeling import make_forecast, generate_trajectory
from modules.forecast_visualization import update_graph # Эта функция теперь отвечает за отрисовку
import numpy as np
class AccordionFrame(ttk.Frame):
"""Виджет 'Аккордеон' для группировки настроек."""
def __init__(self, parent, root_window, **kwargs):
super().__init__(parent, **kwargs)
self.parent = parent
self.root_window = root_window
self.panels = {}
self.current_open = None
self.max_button_width = 0
self.header_frame = ttk.Frame(self)
self.header_frame.pack(fill="x", padx=10, pady=(10, 0))
self.content_frame = ttk.Frame(self)
self.content_frame.pack(fill="both", expand=True, padx=10, pady=5)
def add_panel(self, title, content_frame):
if title in self.panels:
return
self.panels[title] = {'frame': content_frame, 'state': 'closed'}
def on_click():
if self.current_open == title:
self._hide_panel(title)
self.current_open = None
else:
if self.current_open:
self._hide_panel(self.current_open)
self._show_panel(title)
self.current_open = title
btn = ttk.Button(self.header_frame, text=f"+ {title}", command=on_click)
try:
temp_label = tk.Label(self.root_window)
text_width = temp_label.winfo_font().measure(f"+ {title}")
temp_label.destroy()
if text_width > self.max_button_width:
self.max_button_width = text_width
for panel_info in self.panels.values():
if 'button' in panel_info:
panel_info['button'].config(width=self.max_button_width)
except Exception:
pass
btn.pack(anchor='w', pady=2, fill='x')
self.panels[title]['button'] = btn
def _show_panel(self, title):
panel_info = self.panels[title]
panel_info['frame'].pack(fill="both", expand=True, padx=10, pady=10)
panel_info['button'].config(text=f"- {title}")
def _hide_panel(self, title):
panel_info = self.panels[title]
panel_info['frame'].pack_forget()
panel_info['button'].config(text=f"+ {title}")
class ForecastWindow:
"""
Окно для отображения прогноза.
Принимает на вход родительское окно (root) и менеджер проектов.
"""
def __init__(self, parent, legend_checkbox_widget=None, project_manager=None):
self.root_window = parent
self.project_manager = project_manager
self.window = tk.Toplevel(parent)
self.window.title("Прогнозирование")
self.window.geometry("900x700")
self.window.resizable(True, True)
main_container = ttk.Frame(self.window)
main_container.pack(fill="both", expand=True)
main_container.grid_rowconfigure(0, weight=4) # Верхняя часть выше
main_container.grid_rowconfigure(1, weight=1) # Нижняя часть ниже
top_block = ttk.Frame(main_container)
top_block.grid(row=0, column=0, sticky="nsew")
top_block.grid_columnconfigure(0, weight=3) # График шире
top_block.grid_columnconfigure(1, weight=2) # Настройки уже
graph_frame = ttk.LabelFrame(top_block, text="График прогноза", padding="10")
graph_frame.grid(row=0, column=0, sticky="nsew")
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
self.figure = Figure(figsize=(5, 4), dpi=100)
self.ax = self.figure.add_subplot(111)
self.canvas = FigureCanvasTkAgg(self.figure, master=graph_frame)
self.canvas.draw()
self.canvas.get_tk_widget().pack(fill="both", expand=True)
self.accordion = AccordionFrame(top_block, self.window)
self.accordion.grid(row=0, column=1, sticky="ns")
# --- Панель 1: Основные настройки ---
project_panel = ttk.Frame(self.accordion.content_frame)
project_frame = ttk.Frame(project_panel)
project_frame.pack(fill="x", pady=(0, 10))
ttk.Label(project_frame, text="Выбрать проект:").pack(side="left", anchor="w", padx=(0, 5))
self.project_combobox = ttk.Combobox(project_frame, state="readonly", width=25)
self.project_combobox.pack(side="left", anchor="w")
self.project_combobox.bind("<<ComboboxSelected>>", lambda event: self.update_start_date())
self.refresh_button = ttk.Button(project_panel, text="Обновить данные", command=self.load_projects)
self.refresh_button.pack(pady=(5, 15))
date_frame = ttk.Frame(project_panel)
date_frame.pack(fill="x", pady=(0, 10))
ttk.Label(date_frame, text="Дата начала:").pack(side="left", anchor="w", padx=(0, 5))
self.start_date_picker = DateEntry(
date_frame,
width=18,
background='darkblue',
foreground='white',
borderwidth=2,
date_pattern='y-mm-dd'
)
self.start_date_picker.pack(side="left", anchor="w")
s_max_frame = ttk.Frame(project_panel)
s_max_frame.pack(fill="x", pady=(0, 10))
ttk.Label(s_max_frame, text="Верхняя граница (Smax):").pack(side="left", anchor="w", padx=(0, 5))
self.s_max_entry = ttk.Entry(s_max_frame, width=10)
self.s_max_entry.insert(0, "95")
self.s_max_entry.pack(side="left", anchor="w")
s_min_frame = ttk.Frame(project_panel)
s_min_frame.pack(fill="x")
ttk.Label(s_min_frame, text="Нижняя граница (Smin):").pack(side="left", anchor="w", padx=(0, 5))
self.s_min_entry = ttk.Entry(s_min_frame, width=10)
self.s_min_entry.insert(0, "5")
self.s_min_entry.pack(side="left", anchor="w")
self.accordion.add_panel("Основные настройки", project_panel)
# --- Панель 2: Краткосрочный прогноз ---
model_panel = ttk.Frame(self.accordion.content_frame)
self.manual_input_var = tk.BooleanVar(value=True)
ttk.Checkbutton(model_panel, text="Ручной ввод", variable=self.manual_input_var,
command=self.toggle_manual_mode).pack(pady=(5, 10))
intensity_frame = ttk.Frame(model_panel)
intensity_frame.pack(fill="x")
ttk.Label(intensity_frame, text="Интенсивность (в день):").pack(side="left")
self.lambda_entry = ttk.Entry(intensity_frame, width=10)
self.lambda_entry.insert(0, "5")
self.lambda_entry.pack(side="left", padx=5)
avg_frame = ttk.Frame(model_panel)
avg_frame.pack(fill="x")
ttk.Label(avg_frame, text="Среднее за:").pack(side="left")
self.avg_combobox = ttk.Combobox(avg_frame, state='readonly', values=[3, 5, 7, 10, 30], width=5)
self.avg_combobox.current(2)
self.avg_combobox.pack(side="left", padx=5)
period_frame = ttk.Frame(model_panel)
period_frame.pack(fill="x", pady=(5, 15))
ttk.Label(period_frame, text="Прогноз на:").pack(side="left")
self.period_combobox = ttk.Combobox(
period_frame,
state='readonly',
values=["Месяц (30 дней)", "Квартал (90 дней)", "Полгода (180 дней)"],
width=25
)
self.period_combobox.current(0)
self.period_combobox.pack(side="left")
self.accordion.add_panel("Краткосрочный прогноз", model_panel)
self.toggle_manual_mode() # Вызываем вручную для начальной блокировки полей
# --- Панель 3: Настройки симуляции ---
sim_panel = ttk.Frame(self.accordion.content_frame)
sim_settings_frame = ttk.Frame(sim_panel)
sim_settings_frame.pack(fill="x", padx=10, pady=(5, 25))
ttk.Label(sim_settings_frame, text="Кол-во итераций:", justify="left").pack(
side="left", anchor="nw", padx=(0, 10))
self.iterations_entry = ttk.Entry(sim_settings_frame, width=25)
self.iterations_entry.insert(0, "10000")
self.iterations_entry.pack(side="left", anchor="w")
self.accordion.add_panel("Настройки симуляции", sim_panel)
# --- Панель 4: Настройка графика ---
panel4 = ttk.Frame(self.accordion.content_frame)
step_days_frame = ttk.Frame(panel4)
step_days_frame.pack(fill="x", padx=10)
ttk.Label(step_days_frame, text="Шаг (дни):").pack(side="left")
self.step_entry = ttk.Entry(step_days_frame, width=8)
self.step_entry.insert(0, "1")
self.step_entry.pack(side="left", padx=(5, 25))
step_events_frame = ttk.Frame(panel4)
step_events_frame.pack(fill="x", padx=10)
ttk.Label(step_events_frame, text="Шаг (события):").pack(side="left")
self.step_events_entry = ttk.Entry(step_events_frame, width=8)
self.step_events_entry.insert(0, "1")
self.step_events_entry.pack(side="left")
self.show_legend_var = tk.BooleanVar(value=True)
legend_checkbox = ttk.Checkbutton(
panel4,
text='Показать легенду',
variable=self.show_legend_var,
command=self.redraw_graph
)
legend_checkbox.pack(pady=(25, 15))
panel4.update()
self.accordion.add_panel("Настройка графика", panel4)
# --- Панели-заглушки ---
panel5 = ttk.Frame(self.accordion.content_frame)
tk.Label(panel5, text="Это заглушка для пятой панели.").pack(pady=20)
self.accordion.add_panel("Заголовок 5", panel5)
panel6 = ttk.Frame(self.accordion.content_frame)
tk.Label(panel6, text="Это заглушка для шестой панели.").pack(pady=20)
self.accordion.add_panel("Заголовок 6", panel6)
button_frame = ttk.Frame(top_block)
button_frame.grid(row=1, column=1)
self.build_button = ttk.Button(button_frame, text="Построить прогноз", command=self.run_forecast)
self.build_button.pack(pady=30)
result_frame = ttk.LabelFrame(main_container, text="Результаты расчета", padding="5")
result_frame.grid(row=1, column=0, sticky="ew")
self.result_text = scrolledtext.ScrolledText(result_frame, height=4, wrap='word', state='disabled')
scrollbar = ttk.Scrollbar(result_frame, command=self.result_text.yview)
scrollbar.pack(side="right", fill="y")
self.result_text.configure(yscrollcommand=scrollbar.set)
self.result_text.pack(padx=10, pady=5, fill="x")
self.window.after(100, self.load_projects)
# ---- МЕТОДЫ ДЛЯ ИНТЕРФЕЙСА ----
def toggle_manual_mode(self):
"""Включает/выключает режим ручного ввода параметров."""
if self.manual_input_var.get():
self.lambda_entry.config(state='normal')
self.avg_combobox.config(state='disabled')
else:
self.lambda_entry.config(state='disabled')
self.avg_combobox.config(state='readonly')
def redraw_graph(self):
"""
Перерисовывает график БЕЗ повторного запуска симуляции.
Обновляет только видимость легенды.
"""
if not hasattr(self, '_last_result_data'):
return
current_state = self.show_legend_var.get()
self._last_result_data["show_legend"] = current_state
update_graph(self.ax, self.canvas, self._last_result_data)
# ---- ОСНОВНОЙ МЕТОД РАСЧЕТА ПРОГНОЗА ----
def run_forecast(self):
"""Собирает параметры и запускает расчет."""
self._clear_result_text()
try:
iterations = int(self.iterations_entry.get())
lambda_val = float(self.lambda_entry.get())
selected_period_str = self.period_combobox.get()
days_forward_dict = {
"Месяц (30 дней)": 30,
"Квартал (90 дней)": 90,
"Полгода (180 дней)": 180
}
days_forward = days_forward_dict.get(selected_period_str, 30)
s_min = float(self.s_min_entry.get()) / 100
s_max = float(self.s_max_entry.get()) / 100
results = make_forecast(lambda_val=lambda_val, days_forward=days_forward,
iterations=iterations, s_min=s_min, s_max=s_max)
final_lower = results["lower_bound"]
final_upper = results["upper_bound"]
final_median = results["median"]
output_text = (
f"ПРОГНОЗ ДОСТИЖЕНИЯ ЦЕЛИ:\n\n"
f" Цель (Нижняя граница): {final_lower:.2f}\n\n"
f" Прогнозируемый диапазон значений:\n"
f" Минимум: {final_lower:.2f}\n"
f" Максимум: {final_upper:.2f}\n\n"
)
self._insert_into_result(output_text)
step_days = int(self.step_entry.get())
trajectory_daily, _ = generate_trajectory(lambda_val, days_forward, step_days=step_days)
self._last_result_data = {
"lower_bound": final_lower,
"upper_bound": final_upper,
"median": final_median,
"base_level": trajectory_daily[0],
"delta_history": trajectory_daily,
"show_legend": self.show_legend_var.get()
}
update_graph(self.ax, self.canvas, self._last_result_data)
except ValueError as ve:
error_msg = f"Ошибка ввода: {ve}"
print(error_msg)
self._insert_into_result(f"ОШИБКА: {str(ve)}")
except Exception as e:
error_msg = f"Произошла непредвиденная ошибка: {e}"
print(error_msg)
self._insert_into_result(f"НЕПРЕДВИДЕННАЯ ОШИБКА: {str(e)}")
# ---- МЕТОДЫ РАБОТЫ С ТЕКСТОМ В ПОЛЕ РЕЗУЛЬТАТОВ ----
def _insert_into_result(self, text):
"""Добавляет строку текста в поле вывода результатов."""
self.result_text.config(state='normal')
self.result_text.insert(tk.END, text + "\n")
self.result_text.config(state='disabled')
self.result_text.see(tk.END)
def _clear_result_text(self):
"""Очищает поле вывода результатов перед новым расчетом."""
self.result_text.config(state='normal')
self.result_text.delete(1.0, tk.END)
self.result_text.config(state='disabled')
# ---- МЕТОДЫ РАБОТЫ С ПРОЕКТАМИ ----
def load_projects(self):
"""
Загружает список проектов в Combobox.
НЕ сбрасывает текущий выбор пользователя.
"""
try:
if not self.project_manager:
print("[DEBUG] Менеджер проектов недоступен.")
return False
projects = self.project_manager.list_projects()
if not projects:
print("Нет доступных проектов.")
self.project_combobox['values'] = []
return False
# Сохраняем имя текущего выбранного проекта
current_selection = self.project_combobox.get()
project_names = [p.name for p in projects]
self.project_combobox['values'] = project_names
# Восстанавливаем выбор пользователя, если он еще актуален
if current_selection and current_selection in project_names:
self.project_combobox.set(current_selection)
elif project_names: # Если старый выбор недоступен, выбираем первый
self.project_combobox.current(0)
# Обновляем дату начала для текущего выбранного проекта
self.update_start_date()
return True
except AttributeError as e:
print(f"[DEBUG] Ошибка при загрузке проектов: {e}")
return False
def update_start_date(self):
"""Устанавливает дату создания выбранного проекта в календарь."""
selected_project_name = self.project_combobox.get()
if not selected_project_name or not self.project_manager:
return
projects = self.project_manager.list_projects()
selected_project = next((p for p in projects if p.name == selected_project_name), None)
if not selected_project:
return
try:
from modules.project_database import ProjectDatabase, TABLE_PROJECT_DATA
db = ProjectDatabase(project_code=selected_project.id)
conn = db.connect()
cursor = conn.cursor()
cursor.execute(
f"SELECT created_at FROM {TABLE_PROJECT_DATA} WHERE project_code = ? LIMIT 1",
(selected_project.id,)
)
row = cursor.fetchone()
if row and row[0]:
self.start_date_picker.set_date(row[0])
except Exception as e:
print(f"[DEBUG] Ошибка при получении даты: {e}")
finally:
if 'db' in locals():
db.close()