Загрузка данных


# -*- coding: utf-8 -*-
# gui/forecast_window.py
import tkinter as tk
import sqlite3
from tkinter import ttk
from modules.forecasting import monte_carlo_forecast
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from tkcalendar import DateEntry
from modules.project_database import ProjectDatabase, TABLE_PROJECT_DATA
import numpy as np

# --- НОВЫЙ БЛОК: Класс для виджета Аккордеон (финальный) ---
class AccordionFrame(ttk.Frame):
    """Виджет "Аккордеон" для tkinter с автоматическим выравниванием заголовков."""
    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):
        """Добавляет новую панель в аккордеон. Автоматически выравнивает ширину всех кнопок-заголовков."""
        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

        # --- ИСПРАВЛЕННАЯ ЛОГИКА ИЗМЕРЕНИЯ ---
        # Используем Label для точного измерения ширины текста.
        # Создаем временный Label вне экрана.
        temp_label = tk.Label(self.root_window)
        text_to_measure = f"+ {title}"
        text_width = temp_label.font.measure(text_to_measure)
        temp_label.destroy()

        if text_width > self.max_button_width:
            self.max_button_width = text_width

        # Создаем кнопку с фиксированной шириной
        btn = ttk.Button(self.header_frame, text=text_to_measure, command=on_click, width=self.max_button_width)
        btn.pack(anchor='w', pady=2, fill='x')

        # Применяем новую ширину ко всем существующим кнопкам
        for panel_info in self.panels.values():
            if 'button' in panel_info:
                panel_info['button'].config(width=self.max_button_width)

        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):
        # Сохраняем ссылку на главное окно
        self.root_window = parent

        # Создаем новое окно (Toplevel)
        self.window = tk.Toplevel(parent)
        self.window.title("Прогнозирование")
        self.window.geometry("1050x650")
        self.window.resizable(True, True)
        
        self.project_creation_date = None 
        
        # --- ГЛАВНЫЙ КОНТЕЙНЕР ---
        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.ax.grid(True, linestyle='--', alpha=0.7)

        self.canvas = FigureCanvasTkAgg(self.figure, master=graph_frame)
        self.canvas.draw()
        self.canvas.get_tk_widget().pack(fill="both", expand=True)

        # --- ПРАВАЯ ПАНЕЛЬ: ЗАМЕНА НА АККОРДЕОН ---
        # ИСПРАВЛЕНО: Передаем self.window как второй аргумент
        self.accordion = AccordionFrame(top_block, self.window)
        self.accordion.grid(row=0, column=1, sticky="ns")

        # --- СОЗДАЕМ ФРЕЙМЫ ДЛЯ КАЖДОЙ ПАНЕЛИ АККОРДЕОНА ---

        # Панель 1: Основные настройки (ВЕРСТКА В 1 КОЛОНКУ)
        project_panel = ttk.Frame(self.accordion.content_frame)

        # --- Блок 1: Выбрать проект ---
        project_frame = ttk.Frame(project_panel)
        project_frame.pack(fill="x", pady=(0, 10)) # pady добавит отступ между блоками

        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.refresh_button = ttk.Button(project_panel, text="Обновить данные", command=self.update_project_data)
        self.refresh_button.pack() # Кнопка остается в основном фрейме, но под блоком выбора

        # Блок - Начало и Календарь
        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))
        # Создаем календарь БЕЗ начальной даты (initialdate=None)
        self.start_date_picker = DateEntry(
            date_frame,
            width=18,
            background='darkblue',
            foreground='white',
            borderwidth=2,
            date_pattern='y-mm-dd',
            initialdate=None # <-- ВАЖНОЕ ИЗМЕНЕНИЕ
        )
        self.start_date_picker.pack(side="left", anchor="w")

        # --- Блок 3: Верхняя граница (Smax) ---
        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")

        # --- Блок 4: Нижняя граница (Smin) ---
        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)

        input_frame = ttk.Frame(model_panel)
        input_frame.pack(fill="x", padx=10, pady=(5, 0))

        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(model_panel)
        period_frame.pack(fill="x", padx=10, pady=(5, 15))

        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=25)
        self.period_combobox.current(0)
        self.period_combobox.pack(side="left", anchor="w")

        self.accordion.add_panel("Краткосрочный прогноз", model_panel)

        # Панель 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="Кол-во итераций:\n(в симуляции)", 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)
        tk.Label(panel4, text="Это заглушка для четвертой панели. Здесь будет функционал.").pack(pady=20)
        self.accordion.add_panel("Настройка графика", panel4)

        # Панель 5: Заголовок 5
        panel5 = ttk.Frame(self.accordion.content_frame)
        tk.Label(panel5, text="Это заглушка для пятой панели. Здесь будет функционал.").pack(pady=20)
        self.accordion.add_panel("Заголовок 5", panel5)

        # Панель 6: Заголовок 6
        panel6 = ttk.Frame(self.accordion.content_frame)
        tk.Label(panel6, text="Это заглушка для шестой панели. Здесь будет функционал.").pack(pady=20)
        self.accordion.add_panel("Заголовок 6", panel6)


         
        # --- КНОПКА "ПОСТРОИТЬ ПРОГНОЗ" ---
        # Размещаем её под аккордеоном в той же колонке (column=1), но в новой строке (row=1)
        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 = 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")
        
        self.window.after(100, self.load_projects)

    def update_project_data(self):
        """
        Обновляет данные проекта И список доступных проектов.
        """
        self._insert_into_result("--- Начало обновления данных проекта ---")
        
        # 1. Сначала обновляем список проектов в выпадающем меню
        success = self.load_projects()
        if not success:
            self._insert_into_result("ОШИБКА: Не удалось обновить список проектов.")
            return

        # 2. Проверяем, выбран ли проект из обновленного списка
        selected_project_name = self.project_combobox.get()
        if not selected_project_name:
            self._insert_into_result("ОШИБКА: Выберите проект из списка.")
            return

        # 3. Если проект выбран, приступаем к обновлению его данных
        try:
            # Находим объект проекта по имени, чтобы получить его код (ID)
            projects = self.root_window.project_manager.list_projects()
            selected_project = next((p for p in projects if p.name == selected_project_name), None)
            
            if not selected_project:
                raise ValueError(f"Проект '{selected_project_name}' не найден в менеджере.")
                
            project_code = selected_project.id
            self._insert_into_result(f"Код проекта: {project_code}")

            # Создаем экземпляр класса для работы с базой данных проекта
            db = ProjectDatabase(project_code=project_code)
            conn = db.connect() # Устанавливаем соединение

            self._insert_into_result("Соединение с базой данных установлено.")

            # --- ЗДЕСЬ НАЧИНАЕТСЯ ЛОГИКА ОБНОВЛЕНИЯ ---
            cursor = conn.cursor()
            
            # ПРИМЕР: Проверяем структуру таблицы
            cursor.execute(f"PRAGMA table_info({TABLE_PROJECT_DATA})") 
            columns = [col[1] for col in cursor.fetchall()]
            self._insert_into_result(f"Текущие колонки в таблице: {columns}")

            # --- КОНЕЦ ЛОГИКИ ОБНОВЛЕНИЯ ---

            self._insert_into_result("Данные успешно обновлены!")

        # Блок 'except' должен быть на том же уровне отступа, что и 'try'
        except Exception as e:
            # В случае любой ошибки выводим её текст
            error_msg = f"Ошибка при обновлении данных: {e}"
            print(error_msg)
            self._insert_into_result("ERROR: " + error_msg)
        finally:
            # Убедимся, что соединение закрыто, даже если произошла ошибка
            if 'db' in locals():
                db.close()

    def _insert_into_result(self, text):
        """Вставляет текст в поле результатов."""
        self.result_text.config(state='normal')
        self.result_text.insert(tk.END, text + "\n") # Добавляем \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):
        """
        Загружает список проектов и устанавливает дату начала выбранного проекта.
        """
        try:
            # 1. Получаем список всех проектов из главного окна
            projects = self.root_window.project_manager.list_projects()
            if not projects:
                print("Нет доступных проектов.")
                self.project_combobox['values'] = [] # Очищаем список на всякий случай
                return False

            # 2. Создаем списки имен и кодов проектов
            project_names = [p.name for p in projects]
            project_ids = [p.id for p in projects]

            # 3. Заполняем выпадающий список именами
            self.project_combobox['values'] = project_names

            # 4. Проверяем, есть ли уже выбранный проект
            selected_index = self.project_combobox.current()
            if selected_index != -1 and project_ids:
                # Если проект выбран, получаем его код (ID)
                selected_project_id = project_ids[selected_index]

                # --- НАЧАЛО БЛОКА ПОЛУЧЕНИЯ ДАТЫ ИЗ БАЗЫ ДАННЫХ ---
                try:
                    db = ProjectDatabase(project_code=selected_project_id)
                    conn = db.connect()
                    cursor = conn.cursor()

                    # Запрашиваем дату создания из таблицы project_data
                    cursor.execute(
                        "SELECT created_at FROM project_data WHERE project_code = ? LIMIT 1",
                        (selected_project_id,)
                    )
                    row = cursor.fetchone()

                    # Если дата найдена, сохраняем её в атрибут класса
                    if row and row[0]:
                        self.project_creation_date = row[0] # Дата в формате 'YYYY-MM-DD'
                        print(f"[DEBUG] Дата создания проекта: {self.project_creation_date}")
                    else:
                        self.project_creation_date = None
                        print("[DEBUG] Дата создания не найдена в БД.")

                except Exception as e:
                    print(f"[DEBUG] Ошибка при получении даты: {e}")
                    self.project_creation_date = None
                finally:
                    # Важно: всегда закрываем соединение с БД
                    if 'db' in locals():
                        db.close()
                # --- КОНЕЦ БЛОКА ПОЛУЧЕНИЯ ДАТЫ ---


                # --- НАЧАЛО БЛОКА УСТАНОВКИ ДАТЫ В КАЛЕНДАРЬ ---
                # Проверяем, удалось ли нам получить дату из базы
                if self.project_creation_date:
                    # Если дата есть, устанавливаем её в виджет календаря
                    self.start_date_picker.set_date(self.project_creation_date)
                    print(f"[DEBUG] Дата установлена в календаре: {self.project_creation_date}")
                # --- КОНЕЦ БЛОКА УСТАНОВКИ ДАТЫ В КАЛЕНДАРЬ ---

            return True

        except AttributeError as e:
            print(f"Ошибка: Не найден project_manager в главном окне. {e}")
            return False

    def run_forecast(self):
        """Запускает процесс прогнозирования и отрисовывает результаты."""
        self._clear_result_text()

        try:
            # --- ВАЛИДАЦИЯ ВХОДНЫХ ДАННЫХ ---
            # Считываем и проверяем числовые параметры
            iterations = int(self.iterations_entry.get())
            lambda_val = float(self.lambda_entry.get())
            step_days = int(self.step_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)

            # Считываем и проверяем границы (Smin и Smax)
            s_min_str = self.s_min_entry.get().replace(',', '.')
            s_max_str = self.s_max_entry.get().replace(',', '.')

            s_min = float(s_min_str)
            s_max = float(s_max_str)

            if not (0 <= s_min <= 100) or not (0 <= s_max <= 100):
                raise ValueError("Границы (Smin и Smax) должны быть в диапазоне от 0 до 100.")

            if s_min >= s_max:
                raise ValueError("Нижняя граница (Smin) должна быть строго меньше верхней (Smax).")

            # Переводим проценты в доли для функции прогноза
            p1 = s_min / 100.0
            p2 = s_max / 100.0

            # Проверяем, что все основные параметры положительны
            if iterations <= 0 or lambda_val <= 0 or days_forward <= 0 or step_days <= 0:
                raise ValueError("Все параметры (итерации, интенсивность, период, шаг) должны быть больше нуля.")

            # --- ГЕНЕРАЦИЯ ТРАЕКТОРИИ И СИМУЛЯЦИЯ ---
            from datetime import datetime, timedelta
            import numpy as np

            # Пул возможных значений для одного события
            value_pool_positive = [1, 2, 3, 4, 5]
            
            trajectory_daily = []  # История по дням для точного поиска даты
            trajectory_for_plot = [] # История с учетом шага для отрисовки на графике
            
            current_value = 0
            start_date = datetime.now().date()
            
            for day_offset in range(days_forward + 1):
                # Генерируем события за день и суммируем их влияние
                num_events = np.random.poisson(lam=lambda_val)
                daily_sum = sum(np.random.choice(value_pool_positive) for _ in range(num_events))
                current_value += daily_sum
                
                trajectory_daily.append(current_value) # Сохраняем каждый день

                # Добавляем точку на график согласно шагу отображения
                if day_offset % step_days == 0 or day_offset == days_forward:
                    trajectory_for_plot.append(current_value)

                # Отладочный вывод в консоль (можно убрать потом)
                current_date = start_date + timedelta(days=day_offset)
                print(f"{current_date}: Событий: {num_events:2d}. Итог дня: {daily_sum:6.2f}. Накоплено: {current_value:6.2f}")

            if not trajectory_for_plot:
                raise RuntimeError("Не удалось сгенерировать данные для прогноза.")


            # --- РАСЧЕТ ГРАНИЦ ПРОГНОЗА ---
            # Здесь мы используем динамические p1 и p2 вместо фиксированных значений!
            sim_result = monte_carlo_forecast(
                pool_values=trajectory_daily,
                num_simulations=iterations,
                p1=p1,
                p2=p2
            )
            
            final_lower = sim_result["lower_bound"]
            final_upper = sim_result["upper_bound"]
            final_median = sim_result["median"]

            # --- ИЩЕМ ДАТУ ПЕРЕСЕЧЕНИЯ С НИЖНЕЙ ГРАНИЦЕЙ ---
            date_of_intersection = None
            for idx, val in enumerate(trajectory_daily):
                if val >= final_lower:
                    date_of_intersection = start_date + timedelta(days=idx)
                    break

            # --- ФОРМИРУЕМ ТЕКСТ ДЛЯ ВЫВОДА ---
            output_text = (
                f"ПРОГНОЗ ДОСТИЖЕНИЯ ЦЕЛИ:\\n"
                f" Цель (Нижняя граница): {final_lower:.2f}\\n\\n"
                f" Прогнозируемый диапазон значений:\\n"
                f" Минимум: {final_lower:.2f}\\n"
                f" Максимум: {final_upper:.2f}\\n\\n"
            )
            
            if date_of_intersection:
                output_text += f"Достижение минимума прогнозируется: {date_of_intersection.strftime('%d.%m.%Y')}"
            else:
                output_text += "Достижение минимума в заданный период не прогнозируется."

            self._insert_into_result(output_text)

            # --- ПОДГОТОВКА ДАННЫХ ДЛЯ ГРАФИКА ---
            result_data = {
                "lower_bound": final_lower,
                "upper_bound": final_upper,
                "median": final_median,
                "base_level": trajectory_for_plot[0],
                "delta_history": trajectory_for_plot,
                "intersections": {
                    "date": date_of_intersection,
                    "value": final_lower
                }
            }
            
            self.update_graph(result_data)

        except ValueError as e:
            error_msg = f"Ошибка ввода: {e}. Проверьте все поля."
            print(error_msg)
            self._insert_into_result("ОШИБКА: " + str(e))
        except Exception as e:
            error_msg = f"Произошла непредвиденная ошибка: {e}"
            print(error_msg)
            self._insert_into_result("НЕПРЕДВИДЕННАЯ ОШИБКА: " + str(e))
 
    def get_project_history(self):
        """
        Получает историю проекта из БД.
        Этот метод может требовать доработки под вашу структуру базы данных.
        """
        print("--- Режим работы с реальной базой данных ---")
        
        selected_index = self.project_combobox.current()
        if selected_index == -1:
            print("Ошибка: Не выбран проект.")
            return None

        projects = self.root_window.project_manager.list_projects()
        try:
            selected_project_id = projects[selected_index].id
        except IndexError:
            print("Ошибка: Проект не найден в списке.")
            return None

        try:
            # --- ИСПРАВЛЕННЫЙ БЛОК ---
            # Весь код внутри этого 'try' должен иметь одинаковый отступ
            conn = sqlite3.connect(f"project_dbs/{selected_project_id}.db")
            cursor = conn.cursor()
            cursor.execute("""
                SELECT event_sign FROM project_data WHERE project_code = ?
            """, (selected_project_id,))
            rows = cursor.fetchall()
            conn.close()
            
            delta_history = [row[0] for row in rows if row[0] is not None]
            print(f"Динамика из БД: {delta_history}")
            return {"base_level": 0, "delta_history": delta_history}
            # --- КОНЕЦ ИСПРАВЛЕННОГО БЛОКА ---
            
        except Exception as e:
            # Блок 'except' находится на том же уровне, что и 'try'
            print(f"Ошибка при работе с базой данных: {e}")
            return None

    def update_graph(self, result_data):
        """
        Обновляет график на основе переданных данных.
        """
        lower_bound = result_data['lower_bound']
        median = result_data['median']
        upper_bound = result_data['upper_bound']
        base_level = result_data['base_level']
        trajectory = result_data['delta_history']
        
        # Очищаем график
        self.ax.clear()
        
        # Рисуем горизонтальные линии границ и медианы
        if lower_bound is not None:
            self.ax.axhline(y=lower_bound, color='red', linestyle='--', label=f'Нижняя граница')
        if median is not None:
            self.ax.axhline(y=median , color='green', linestyle='-', label=f'Медиана')
        if upper_bound is not None:
            self.ax.axhline(y=upper_bound , color='blue', linestyle='--', label=f'Верхняя граница')
        
        # Рисуем траекторию (линию графика), если есть данные
        if trajectory and len(trajectory) > 1:
            from datetime import datetime, timedelta
            
            start_date = datetime.now().date()
            step_days = int(self.step_entry.get())
            
            dates_for_plot = []
            for i in range(len(trajectory)):
                day_offset = i * step_days
                plot_date = start_date + timedelta(days=day_offset)
                dates_for_plot.append(plot_date)
            
            # Сама линия траектории
            self.ax.plot(dates_for_plot, trajectory,
                         color='orange', marker='o',
                         linewidth=2, markersize=6,
                         label='Смоделированная динамика')
            
            # Настраиваем ось X с датами
            self.ax.set_xticks(dates_for_plot)
            self.ax.set_xticklabels([d.strftime('%d.%m') for d in dates_for_plot])
        
        # Настраиваем внешний вид графика
        self.ax.set_ylabel('Накопленное значение')
        self.ax.set_xlabel('Дата')
        self.ax.grid(True, linestyle='--', alpha=0.7)
        self.ax.legend(loc='upper left')
        
        # Перерисовываем canvas ОЧЕНЬ ВАЖНО!
        self.canvas.draw()