Загрузка данных
# -*- coding: utf-8 -*-
# gui/forecast_window.py
import tkinter as tk
from tkinter import ttk
from datetime import datetime, timedelta
import matplotlib.dates as mdates
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
class SettingsAccordionPanel(ttk.Frame):
"""Класс для правой панели настроек."""
def __init__(self, parent, main_window_ref=None):
super().__init__(parent)
self.main_window = main_window_ref
s = ttk.Style()
# Стиль делает табы похожими на кнопки слева
s.layout('Accordion.TNotebook.Tab', [
('Accordion.Toolbutton', {'children': [('Label', {'side': 'left'})], 'sticky': 'nswe'})
])
s.configure('Accordion.TNotebook', tabposition='wn')
s.map('Accordion.TNotebook', background=[('selected', '!focus', '#f0f0f0')])
s.configure('Accordion.TNotebook.Tab', padding=[12, 6, 12, 6], relief="flat") # <-- Выравнивание заголовков
self.frame = ttk.Notebook(self, style='Accordion.TNotebook')
self.frame.pack(expand=1, fill="both")
for title in ["Общие настройки", "Настройки симуляции", "Настройки агрегирования",
"Настройка статистики", "Настройка печати"]:
frame = ttk.Frame(self.frame, padding=10)
self.frame.add(frame, text=title)
# --- Виджеты ТОЛЬКО для "Настроек симуляции" ---
sim_tab = self.frame.tabs()[1]
sim_frame = self.frame.nametowidget(sim_tab)
sim_frame.columnconfigure(0, weight=3)
sim_frame.columnconfigure(1, weight=2)
row_num = 0
ttk.Label(sim_frame, text="Интенсивность событий (в день):").grid(row=row_num, column=0, sticky="e", padx=(0,5), pady=2)
self.lambda_entry = ttk.Entry(sim_frame)
self.lambda_entry.insert(0, "5")
self.lambda_entry.grid(row=row_num, column=1, sticky="we", pady=2)
row_num += 1
ttk.Label(sim_frame, text="Шаг (дни):").grid(row=row_num, column=0, sticky="e", padx=(0,5), pady=2)
self.step_entry = ttk.Entry(sim_frame)
self.step_entry.insert(0, "1")
self.step_entry.grid(row=row_num, column=1, sticky="we", pady=2)
row_num += 1
period_frame = ttk.Frame(sim_frame)
period_frame.grid(row=row_num, column=0, columnspan=2, sticky="we", pady=2)
ttk.Label(period_frame, text="Прогноз на:").pack(side="left", anchor="w", padx=(0, 5))
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")
row_num += 1
ttk.Label(sim_frame, text="Кол-во итераций:\n(в симуляции)").grid(row=row_num, column=0, sticky="ne", pady=2)
self.iterations_entry = ttk.Entry(sim_frame)
self.iterations_entry.insert(0, "10000")
self.iterations_entry.grid(row=row_num, column=1, sticky="we", pady=2)
row_num += 1
# Кнопка "Рассчитать"
self.build_button = ttk.Button(sim_frame, text="Рассчитать", command=self.on_build_button_clicked)
self.build_button.grid(row=row_num, column=0, columnspan=2, sticky="we", pady=20, ipadx=10, ipady=5)
def on_build_button_clicked(self):
if self.main_window is not None:
self.main_window.run_forecast()
class ResultsPanel(ttk.Frame):
"""Компонент для нижней части окна: результаты и кнопки."""
def __init__(self, parent, height):
super().__init__(parent)
self.frame = ttk.Frame(self)
self.frame.pack(fill="x")
result_frame = ttk.Frame(self.frame)
result_frame.pack(side="left", fill="both", expand=True)
self.result_text = tk.Text(result_frame, height=height, 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", expand=True)
button_frame = ttk.Frame(self.frame)
button_frame.pack(side="right", padx=10)
self.calculate_btn = ttk.Button(button_frame, text="Рассчитать", state="disabled")
self.print_btn = ttk.Button(button_frame, text="Печатать в файл")
self.calculate_btn.pack(pady=5, ipadx=10, side="top")
self.print_btn.pack(pady=5, ipadx=10, side="top")
def set_calculate_command(self, command):
self.calculate_btn.config(command=command)
self.calculate_btn.config(state="normal")
class ForecastWindow:
"""Главное окно приложения."""
def __init__(self, parent):
self.root_window = parent
self.window = tk.Toplevel(parent)
self.window.title("Прогнозирование")
self.window.geometry("1050x700")
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", padx=5, pady=(5, 0))
# Конфигурация сетки верхнего блока: две колонки
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="nswe")
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.get_tk_widget().pack(fill="both", expand=True)
# 2. Правая панель: Аккордеон
self.settings_accordion = SettingsAccordionPanel(top_block, main_window_ref=self)
self.settings_accordion.grid(row=0, column=1, sticky="nswe", padx=(10, 0))
# Принудительное обновление геометрии
self.window.update_idletasks()
# --- НИЖНИЙ БЛОК: Результаты расчета и Кнопки ---
self.results_panel = ResultsPanel(main_container, height=4)
self.results_panel.frame.grid(row=1, column=0, sticky="ew", pady=(5, 5))
# Связываем кнопку из аккордеона с полем результатов
self.results_panel.set_calculate_command(self.settings_accordion.on_build_button_clicked)
# Устанавливаем минимальный размер окна
min_width = max(1050, self.window.winfo_reqwidth())
min_height = max(650, self.window.winfo_reqheight())
self.window.minsize(min_width, min_height)
# ... остальные методы остаются без изменений ...
def _insert_into_result(self, text):
self.results_panel.result_text.config(state='normal')
self.results_panel.result_text.insert(tk.END, text + "\n")
self.results_panel.result_text.config(state='disabled')
self.results_panel.result_text.see(tk.END)
def _clear_result_text(self):
self.results_panel.result_text.config(state='normal')
self.results_panel.result_text.delete(1.0, tk.END)
self.results_panel.result_text.config(state='disabled')
def run_forecast(self):
try:
iterations = int(self.settings_accordion.iterations_entry.get())
lambda_val = float(self.settings_accordion.lambda_entry.get())
selected_period = self.settings_accordion.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.settings_accordion.step_entry.get())
start_date = datetime.now().date()
from modules.forecast_data_processing import generate_trajectory
trajectory, daily_values_history = generate_trajectory(lambda_val, days_forward, step_days)
if not trajectory:
self._insert_into_result("Не удалось сгенерировать данные.")
return
from modules.forecast_modeling import monte_carlo_forecast
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"]
output_text = (
f"ПРОГНОЗ ДОСТИЖЕНИЯ ЦЕЛИ:\\n"
f" Цель (Нижняя граница): {final_lower:.2f}\\n\\n"
f" Прогнозируемый диапазон значений:\\n"
f" Минимум: {final_lower:.2f}\\n"
f" Максимум: {final_upper:.2f}\\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 += "Достижение минимума в заданный период не прогнозируется."
from modules.forecast_visualization import GraphUpdater
graph_updater = GraphUpdater(start_date, step_days)
plot_data = graph_updater.prepare_plot_data(sim_result, trajectory, daily_values_history)
self._update_results_text(output_text)
self._draw_graph(plot_data)
except Exception as e:
print(e)
self._insert_into_result(f"ОШИБКА: {e}")
def _update_results_text(self, output_text):
self._clear_result_text()
self._insert_into_result(output_text)
def _draw_graph(self, plot_data):
self.ax.clear()
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='Смоделированная динамика')
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')
self.canvas.draw()