Загрузка данных
# границы прогноза max. мин.
# начало прогноза (с начала проекта, с указанной даты)
# приклеить низ
#
#
# -*- coding: utf-8 -*-
# gui/forecast_window.py
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
import matplotlib.dates as mdates
from tkinter import ttk
from datetime import datetime, timedelta
# --- ИМПОРТЫ НОВЫХ МОДУЛЕЙ ---
from modules.forecast_data_processing import generate_trajectory
from modules.forecast_modeling import monte_carlo_forecast
from modules.forecast_visualization import GraphUpdater
class ForecastWindow:
"""
Окно для отображения прогноза.
Принимает на вход родительское окно (root), чтобы иметь доступ к его данным.
"""
def __init__(self, parent):
# Сохраняем ссылку на главное окно
self.root_window = parent
# Создаем новое окно (Toplevel)
self.window = tk.Toplevel(parent)
self.window.title("Прогнозирование")
self.window.geometry("1050x650")
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) # Настройки уже
# 1. Левая панель: График
graph_frame = ttk.LabelFrame(top_block, text="График прогноза", padding="10")
graph_frame.grid(row=0, column=0, sticky="nsew")
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)
# --- ПРАВАЯ ПАНЕЛЬ ---
settings_frame = ttk.LabelFrame(top_block, text="Параметры модели", padding="10")
settings_frame.grid(row=0, column=1, sticky="ns")
settings_frame.config(width=500)
settings_frame.pack_propagate(False)
# -- Блок выбора проекта --
project_frame = ttk.Frame(settings_frame)
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=28)
self.project_combobox.pack(side="left", anchor="w")
self.refresh_button = ttk.Button(project_frame, text="Обновить список", command=self.load_projects)
self.refresh_button.pack(side="left", anchor="w", padx=(10, 0))
self.load_projects()
# -- Поля ввода параметров --
input_frame = ttk.Frame(settings_frame)
input_frame.pack(fill="x", padx=10, pady=5)
ttk.Label(input_frame, text="Интенсивность событий (в день):").pack(side="left", anchor="w", padx=(0, 10))
self.lambda_entry = ttk.Entry(input_frame, width=10)
self.lambda_entry.insert(0, "5")
self.lambda_entry.pack(side="left", anchor="w")
ttk.Label(input_frame, text="Шаг (дни):").pack(side="left", anchor="w", padx=(20, 10))
self.step_entry = ttk.Entry(input_frame, width=10)
self.step_entry.insert(0, "1")
self.step_entry.pack(side="left", anchor="w")
# Поле "Прогноз на:"
period_frame = ttk.Frame(settings_frame)
period_frame.pack(fill="x", padx=10, pady=(5, 0))
ttk.Label(period_frame, text="Прогноз на:").pack(side="left", anchor="w", padx=(0, 10))
self.period_combobox = ttk.Combobox(period_frame, state="readonly",
values=["Месяц (30 дней)", "Квартал (90 дней)", "Полгода (180 дней)"],
width=18)
self.period_combobox.current(0)
self.period_combobox.pack(side="left", anchor="w")
# -- Настройки симуляции --
sim_settings_frame = ttk.Frame(settings_frame)
sim_settings_frame.pack(fill="x", padx=10, pady=5)
ttk.Label(sim_settings_frame, text="Кол-во итераций:\n(в симуляции)", justify="left").pack(
side="left", anchor="nw", padx=(0, 10))
self.iterations_entry = ttk.Entry(sim_settings_frame, width=15)
self.iterations_entry.insert(0, "10000")
self.iterations_entry.pack(side="left", anchor="w")
self.build_button = ttk.Button(settings_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 = tk.Text(result_frame, height=2, 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")
def load_projects(self):
"""Загружает список проектов из главного окна в выпадающий список."""
self.project_combobox.set('')
projects = self.root_window.project_manager.list_projects()
project_names = [p.name for p in projects]
self.project_combobox['values'] = project_names
if project_names:
self.project_combobox.current(0)
def _insert_into_result(self, text):
"""Вставляет текст в поле результатов."""
self.result_text.config(state='normal')
self.result_text.insert(tk.END, text)
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 run_forecast(self):
"""Финальная рабочая версия метода."""
# Очищаем поле результатов и график перед новым расчетом
self._clear_result_text()
try:
iterations = int(self.iterations_entry.get())
lambda_val = float(self.lambda_entry.get())
selected_period = self.period_combobox.get()
days_forward_dict = {"Месяц": 30, "Квартал": 90, "Полгода": 180}
period_key = next((k for k in days_forward_dict if k in selected_period), None)
days_forward = days_forward_dict.get(period_key, 30)
step_days = int(self.step_entry.get())
from datetime import datetime
start_date = datetime.now().date()
except ValueError as e:
error_msg = f"Ошибка ввода: {e}. Проверьте все поля."
print(error_msg)
self._insert_into_result("ОШИБКА: " + str(e) + "\n")
return
# --- 1. РАБОТА С ДАННЫМИ И МОДЕЛЬЮ ---
from modules.forecast_data_processing import generate_trajectory
from modules.forecast_modeling import monte_carlo_forecast
from modules.forecast_visualization import GraphUpdater
trajectory, daily_values_history = generate_trajectory(lambda_val, days_forward, step_days)
if not trajectory:
self._insert_into_result("Не удалось сгенерировать данные.\n")
return
sim_result = monte_carlo_forecast(
pool_values=trajectory,
num_simulations=iterations,
p1=0.05,
p2=0.95
)
final_lower = sim_result["lower_bound"]
final_upper = sim_result["upper_bound"]
final_median = sim_result["median"]
# --- 2. ПОДГОТОВКА РЕЗУЛЬТАТОВ (ТЕКСТ) ---
output_text = (
f"ПРОГНОЗ ДОСТИЖЕНИЯ ЦЕЛИ:\n"
f" Цель (Нижняя граница): {final_lower:.2f}\n\n"
f" Прогнозируемый диапазон значений:\n"
f" Минимум: {final_lower:.2f}\n"
f" Максимум: {final_upper:.2f}\n\n"
)
# Поиск даты пересечения
date_of_intersection = None
for idx, val in enumerate(daily_values_history):
if val >= final_lower:
date_of_intersection = start_date + timedelta(days=idx)
break
if date_of_intersection:
output_text += f"Достижение минимума прогнозируется: {date_of_intersection.strftime('%d.%m.%Y')}"
else:
output_text += "Достижение минимума в заданный период не прогнозируется."
self._insert_into_result(output_text)
# --- 3. ВИЗУАЛИЗАЦИЯ (ЭТО ГЛАВНАЯ ЧАСТЬ!) ---
# Сначала очищаем оси графика
self.ax.clear()
# Создаем объект-подготовщик
graph_updater = GraphUpdater(start_date, step_days)
# Получаем готовые данные для рисования
plot_data = graph_updater.prepare_plot_data(sim_result, trajectory, daily_values_history)
# --- КОМАНДЫ РИСОВАНИЯ ---
# Рисуем горизонтальные линии (всегда)
self.ax.axhline(y=plot_data["lower_bound"], color='red', linestyle='--', label=f'Нижняя граница')
self.ax.axhline(y=plot_data["median"], color='green', linestyle='-', label=f'Медиана')
self.ax.axhline(y=plot_data["upper_bound"], color='blue', linestyle='--', label=f'Верхняя граница')
# Рисуем траекторию (если есть данные)
if plot_data["has_trajectory"]:
self.ax.plot(plot_data["dates"], plot_data["trajectory"], color='orange',
marker='o', linewidth=2, markersize=6, label='Смоделированная динамика')
# Настраиваем ось X
self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%d.%m'))
self.ax.xaxis.set_tick_params(rotation=45)
self.ax.set_xticks(plot_data["dates"])
# Рисуем точку пересечения (если она есть и есть траектория)
if plot_data["intersection_date"] and plot_data["has_trajectory"]:
x_coord = plot_data["dates"][plot_data["intersection_x_idx"]]
y_coord = plot_data["intersection_y_val"]
self.ax.scatter(x_coord, y_coord, color='darkred', s=150,
zorder=5, edgecolor='black', linewidth=1, alpha=0.7)
self.ax.annotate(f'{plot_data["intersection_date"].strftime("%d.%m")}',
xy=(x_coord, y_coord),
xytext=(5, 5), textcoords="offset points",
fontsize=8, backgroundcolor='white')
# Добавляем легенду
handles, labels = self.ax.get_legend_handles_labels()
if handles:
self.ax.legend(loc='upper left')
# --- 4. ФИНАЛЬНЫЙ ЭТАП ---
# ОБЯЗАТЕЛЬНО сообщаем Canvas, что нужно перерисоваться!
self.canvas.draw()