максимально оптимизимруй приложение даже для маленьких экранов
оптимизируй всё что только можно
также жобавь всяких прикольных фич

import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import matplotlib
import numpy as np
import pandas as pd
import mplcursors  # Для интерактивных подсказок
from scipy.ndimage import gaussian_filter  # Для сглаживания данных
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D  # Для 3D графиков
from matplotlib import cm, colors  # Для управления цветовыми картами

# Используем встроенный Tkinter бэкэнд
matplotlib.use('TkAgg')

class ContourMapApp:
    def __init__(self, master):
        self.master = master
        self.master.title("Контурная Карта")
        self.master.geometry("1600x1000")

        # Инициализация основных переменных
        self.z_original = None  # Оригинальные данные
        self.z = None  # Обработанные данные
        self.x = None
        self.y = None
        self.colorbar = None
        self.current_style = 'ggplot'

        # Инициализация Tkinter переменных
        self.plot_type = tk.StringVar(value='Filled Contours')  # Тип отображения
        self.log_scale = tk.BooleanVar(value=False)  # Логарифмическая шкала
        self.interp_method = tk.StringVar(value='None')  # Метод интерполяции
        self.smooth_var = tk.BooleanVar(value=False)  # Сглаживание данных
        self.smooth_sigma = tk.DoubleVar(value=1.0)  # Параметр сглаживания
        self.color_norm = tk.StringVar(value='linear')  # Нормализация цвета
        self.style = tk.StringVar(value='ggplot')  # Стиль Matplotlib
        self.grid_var = tk.BooleanVar(value=True)  # Отображение сетки

        # Инициализация переменной для статусной панели
        self.status = tk.StringVar()
        self.status.set("Готово")

        # Создание верхней панели с кнопками и настройками
        self.create_top_frame()

        # Создание области для графика
        self.create_plot_area()

        # Добавление меню для дополнительных функций
        self.create_menu()

        # Создание статусной панели
        self.create_status_bar()

    def create_top_frame(self):
        self.top_frame = tk.Frame(self.master)
        self.top_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=10)

        # Кнопка загрузки файла
        self.load_button = tk.Button(self.top_frame, text="Загрузить Файл", command=self.load_file)
        self.load_button.pack(side=tk.LEFT, padx=5)

        # Выбор стиля Matplotlib
        self.style_label = tk.Label(self.top_frame, text="Стиль Графика:")
        self.style_label.pack(side=tk.LEFT, padx=5)

        self.style_menu = ttk.Combobox(
            self.top_frame,
            textvariable=self.style,
            values=sorted(m for m in plt.style.available),
            state='readonly',
            width=15
        )
        self.style_menu.pack(side=tk.LEFT, padx=5)
        self.style_menu.bind("<<ComboboxSelected>>", lambda e: self.update_style())

        # Выбор типа отображения
        self.plot_type_label = tk.Label(self.top_frame, text="Тип Отображения:")
        self.plot_type_label.pack(side=tk.LEFT, padx=5)

        self.plot_type_menu = ttk.Combobox(
            self.top_frame,
            textvariable=self.plot_type,
            values=['Filled Contours', 'Contour Lines', '3D Surface'],
            state='readonly',
            width=15
        )
        self.plot_type_menu.pack(side=tk.LEFT, padx=5)
        self.plot_type_menu.bind("<<ComboboxSelected>>", lambda e: self.update_plot())

        # Выбор цветовой карты
        self.colormap_label = tk.Label(self.top_frame, text="Цветовая карта:")
        self.colormap_label.pack(side=tk.LEFT, padx=5)

        self.colormap_var = tk.StringVar(value='viridis')
        self.colormap_menu = ttk.Combobox(
            self.top_frame,
            textvariable=self.colormap_var,
            values=sorted(m for m in plt.colormaps()),
            state='readonly',
            width=15
        )
        self.colormap_menu.pack(side=tk.LEFT, padx=5)
        self.colormap_menu.bind("<<ComboboxSelected>>", lambda e: self.update_plot())

        # Настройка уровней контуров
        self.levels_label = tk.Label(self.top_frame, text="Уровни:")
        self.levels_label.pack(side=tk.LEFT, padx=5)

        self.levels_entry = tk.Entry(self.top_frame, width=10)
        self.levels_entry.insert(0, "10")
        self.levels_entry.pack(side=tk.LEFT, padx=5)
        self.levels_entry.bind("<Return>", lambda e: self.update_plot())

        # Поля для заголовка и меток осей
        self.title_label = tk.Label(self.top_frame, text="Заголовок:")
        self.title_label.pack(side=tk.LEFT, padx=5)

        self.title_entry = tk.Entry(self.top_frame, width=15)
        self.title_entry.insert(0, "Контурная Карта")
        self.title_entry.pack(side=tk.LEFT, padx=5)
        self.title_entry.bind("<Return>", lambda e: self.update_plot())

        self.xlabel_label = tk.Label(self.top_frame, text="Ось X:")
        self.xlabel_label.pack(side=tk.LEFT, padx=5)

        self.xlabel_entry = tk.Entry(self.top_frame, width=10)
        self.xlabel_entry.insert(0, "X")
        self.xlabel_entry.pack(side=tk.LEFT, padx=5)
        self.xlabel_entry.bind("<Return>", lambda e: self.update_plot())

        self.ylabel_label = tk.Label(self.top_frame, text="Ось Y:")
        self.ylabel_label.pack(side=tk.LEFT, padx=5)

        self.ylabel_entry = tk.Entry(self.top_frame, width=10)
        self.ylabel_entry.insert(0, "Y")
        self.ylabel_entry.pack(side=tk.LEFT, padx=5)
        self.ylabel_entry.bind("<Return>", lambda e: self.update_plot())

        # Настройка отображения сетки
        self.grid_check = tk.Checkbutton(
            self.top_frame, text="Сетка", variable=self.grid_var, command=self.update_plot
        )
        self.grid_check.pack(side=tk.LEFT, padx=5)

        # Опция логарифмической шкалы
        self.log_check = tk.Checkbutton(
            self.top_frame, text="Лог. шкала Z", variable=self.log_scale, command=self.update_plot
        )
        self.log_check.pack(side=tk.LEFT, padx=5)

        # Настройка метода интерполяции
        self.interp_label = tk.Label(self.top_frame, text="Интерполяция:")
        self.interp_label.pack(side=tk.LEFT, padx=5)

        self.interp_menu = ttk.Combobox(
            self.top_frame,
            textvariable=self.interp_method,
            values=['None', 'nearest', 'linear', 'cubic'],
            state='readonly',
            width=10
        )
        self.interp_menu.pack(side=tk.LEFT, padx=5)
        self.interp_menu.bind("<<ComboboxSelected>>", lambda e: self.update_plot())

        # Опции сглаживания данных
        self.smooth_check = tk.Checkbutton(
            self.top_frame, text="Сгладить данные", variable=self.smooth_var, command=self.update_plot
        )
        self.smooth_check.pack(side=tk.LEFT, padx=5)

        self.smooth_label = tk.Label(self.top_frame, text="Sigma:")
        self.smooth_label.pack(side=tk.LEFT, padx=2)

        self.smooth_spinbox = tk.Spinbox(
            self.top_frame, from_=0.1, to=10.0, increment=0.1, textvariable=self.smooth_sigma, width=5,
            command=self.update_plot
        )
        self.smooth_spinbox.pack(side=tk.LEFT, padx=2)
        self.smooth_spinbox.bind("<Return>", lambda e: self.update_plot())

        # Настройка ограничений осей
        self.xlim_label = tk.Label(self.top_frame, text="X пределы:")
        self.xlim_label.pack(side=tk.LEFT, padx=5)

        self.xmin_entry = tk.Entry(self.top_frame, width=7)
        self.xmin_entry.pack(side=tk.LEFT, padx=2)
        self.xmin_entry.insert(0, "")

        self.xmax_entry = tk.Entry(self.top_frame, width=7)
        self.xmax_entry.pack(side=tk.LEFT, padx=2)
        self.xmax_entry.insert(0, "")

        self.ylim_label = tk.Label(self.top_frame, text="Y пределы:")
        self.ylim_label.pack(side=tk.LEFT, padx=5)

        self.ymin_entry = tk.Entry(self.top_frame, width=7)
        self.ymin_entry.pack(side=tk.LEFT, padx=2)
        self.ymin_entry.insert(0, "")

        self.ymax_entry = tk.Entry(self.top_frame, width=7)
        self.ymax_entry.pack(side=tk.LEFT, padx=2)
        self.ymax_entry.insert(0, "")

        self.apply_limits_button = tk.Button(self.top_frame, text="Применить Ограничения", command=self.apply_limits)
        self.apply_limits_button.pack(side=tk.LEFT, padx=5)

        # Кнопка экспорта выбранных данных
        self.export_button = tk.Button(self.top_frame, text="Экспорт Выбранных Данных", command=self.export_selected_region)
        self.export_button.pack(side=tk.LEFT, padx=5)

        # Кнопка сохранения изображения с расширенными опциями
        self.save_button = tk.Button(self.top_frame, text="Сохранить Изображение", command=self.save_plot)
        self.save_button.pack(side=tk.RIGHT, padx=5)

    def create_plot_area(self):
        self.figure = plt.Figure(figsize=(14, 8))
        self.ax = self.figure.add_subplot(111)

        self.canvas = FigureCanvasTkAgg(self.figure, master=self.master)
        self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

        # Добавление панели инструментов Matplotlib
        toolbar = matplotlib.backends.backend_tkagg.NavigationToolbar2Tk(self.canvas, self.master)
        toolbar.update()
        self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

    def create_menu(self):
        menubar = tk.Menu(self.master)
        self.master.config(menu=menubar)

        # Меню просмотра
        view_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Вид", menu=view_menu)
        view_menu.add_command(label="Сбросить Масштаб", command=self.reset_view)

        # Меню помощи
        help_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Справка", menu=help_menu)
        help_menu.add_command(label="О программе", command=self.show_about)

    def create_status_bar(self):
        self.status_bar = tk.Label(self.master, textvariable=self.status, bd=1, relief=tk.SUNKEN, anchor=tk.W)
        self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)

    def load_file(self):
        file_path = filedialog.askopenfilename(
            title="Выберите файл с данными",
            filetypes=(("CSV файлы", "*.csv"), ("Текстовые файлы", "*.txt"), ("Excel файлы", "*.xlsx"), ("Все файлы", "*.*"))
        )
        if not file_path:
            self.update_status("Загрузка файла отменена.")
            return

        try:
            # Автоматическое определение разделителя и формата файла
            if file_path.endswith('.xlsx'):
                df = pd.read_excel(file_path, header=None)
            else:
                df = pd.read_csv(file_path, header=None, sep=None, engine='python')

            self.z_original = df.values
            self.y, self.x = self.z_original.shape
            self.z = self.z_original.copy()
            self.update_status("Данные загружены успешно.")
            self.update_plot()
            self.display_statistics()
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось загрузить файл:\n{e}")
            self.update_status("Ошибка при загрузке файла.")

    def display_statistics(self):
        if self.z is None:
            return

        min_val = np.min(self.z)
        max_val = np.max(self.z)
        mean_val = np.mean(self.z)

        stats_message = f"Статистика данных:\nМинимум: {min_val}\nМаксимум: {max_val}\nСреднее: {mean_val:.2f}"
        messagebox.showinfo("Статистика Данныx", stats_message)
        self.update_status("Статистика отображена.")

    def plot_contour(self):
        if self.z is None:
            return

        plot_type = self.plot_type.get()
        levels = self.get_levels()
        cmap_name = self.colormap_var.get()
        show_grid = self.grid_var.get()
        log_scale = self.log_scale.get()
        interp_method = self.interp_method.get()
        smooth = self.smooth_var.get()
        sigma = self.smooth_sigma.get()
        norm_type = self.color_norm.get()

        # Установка заголовка и меток осей
        title = self.title_entry.get()
        xlabel = self.xlabel_entry.get()
        ylabel = self.ylabel_entry.get()

        x = np.arange(self.x)
        y = np.arange(self.y)

        # Настройка интерполяции
        if interp_method != 'None':
            from scipy.interpolate import griddata
            grid_x, grid_y = np.linspace(x.min(), x.max(), self.x * 2), np.linspace(y.min(), y.max(), self.y * 2)
            X, Y = np.meshgrid(grid_x, grid_y)
            Z = self.z_original.copy()
            try:
                self.z = griddata((x.flatten(), y.flatten()), self.z_original.flatten(),
                                  (X, Y), method=interp_method)
            except Exception as e:
                messagebox.showerror("Ошибка", f"Не удалось выполнить интерполяцию:\n{e}")
                self.update_status("Ошибка при интерполяции данных.")
                return
        else:
            X, Y = np.meshgrid(x, y)
            self.z = self.z_original.copy()

        # Применение логарифмической шкалы, если выбрано
        if log_scale:
            # Заменяем нули и отрицательные значения на минимально положительное для логарифма
            if np.any(self.z <= 0):
                min_positive = np.min(self.z[self.z > 0])
                self.z[self.z <= 0] = min_positive
            self.z = np.log(self.z)

        # Применение сглаживания, если выбрано
        if smooth:
            self.z = gaussian_filter(self.z, sigma=sigma)

        # Настройка нормализации цветовой карты
        if norm_type == 'log':
            norm = colors.LogNorm()
        else:
            norm = None

        if plot_type == '3D Surface':
            # Создание 3D осей
            ax = self.figure.add_subplot(111, projection='3d')
            if levels > 0:
                cmap = self.get_discrete_colormap(cmap_name, levels)
            else:
                cmap = cmap_name

            if norm is not None:
                surf = ax.plot_surface(X, Y, self.z, cmap=cmap, norm=norm, edgecolor='none')
            else:
                surf = ax.plot_surface(X, Y, self.z, cmap=cmap, edgecolor='none')
            self.colorbar = self.figure.colorbar(surf, ax=ax, shrink=0.5, aspect=10)
            ax.set_title(title)
            ax.set_xlabel(xlabel)
            ax.set_ylabel(ylabel)
            ax.set_zlabel('Z')
            ax.grid(show_grid)

            # Добавление интерактивных подсказок
            cursor = mplcursors.cursor(surf, hover=True)

            @cursor.connect("add")
            def on_add(sel):
                # Для 3D графика индексация немного сложнее
                # Используем nearest indexing
                x_val = X[int(sel.target.index / self.z.shape[1]), sel.target.index % self.z.shape[1]]
                y_val = Y[int(sel.target.index / self.z.shape[1]), sel.target.index % self.z.shape[1]]
                z_val = self.z[int(sel.target.index / self.z.shape[1]), sel.target.index % self.z.shape[1]]
                sel.annotation.set(text=f"x={x_val}, y={y_val}, z={z_val:.2f}")
        else:
            # Создание 2D осей
            ax = self.figure.add_subplot(111)
            if plot_type == 'Filled Contours':
                contour = ax.contourf(X, Y, self.z, levels=levels, cmap=cmap_name, norm=norm)
            elif plot_type == 'Contour Lines':
                contour = ax.contour(X, Y, self.z, levels=levels, cmap=cmap_name, norm=norm)
                ax.clabel(contour, inline=True, fontsize=8)
            else:
                contour = None

            if contour:
                self.colorbar = self.figure.colorbar(contour, ax=ax)
            ax.set_title(title)
            ax.set_xlabel(xlabel)
            ax.set_ylabel(ylabel)
            ax.grid(show_grid)

            # Добавление интерактивных подсказок
            if contour:
                cursor = mplcursors.cursor(contour, hover=True)

                @cursor.connect("add")
                def on_add(sel):
                    # Получение индекса для 2D графика
                    x_idx = int(sel.target.index / self.z.shape[1])
                    y_idx = sel.target.index % self.z.shape[1]
                    x_val = X[x_idx, y_idx]
                    y_val = Y[x_idx, y_idx]
                    z_val = self.z[x_idx, y_idx]
                    sel.annotation.set(text=f"x={x_val}, y={y_val}, z={z_val:.2f}")

        self.ax = ax

    def get_discrete_colormap(self, cmap_name, levels):
        """
        Создает дискретную цветовую карту с заданным количеством уровней.
        """
        cmap = cm.get_cmap(cmap_name, levels)
        return cmap

    def get_levels(self):
        try:
            levels = int(self.levels_entry.get())
            if levels <= 0:
                raise ValueError
        except ValueError:
            messagebox.showwarning(
                "Предупреждение",
                "Количество уровней должно быть положительным целым числом. Используется значение по умолчанию (10)."
            )
            levels = 10
            self.levels_entry.delete(0, tk.END)
            self.levels_entry.insert(0, "10")
        return levels

    def apply_limits(self):
        if self.z is None:
            messagebox.showwarning("Предупреждение", "Сначала загрузите данные.")
            return

        x_min = self.xmin_entry.get()
        x_max = self.xmax_entry.get()
        y_min = self.ymin_entry.get()
        y_max = self.ymax_entry.get()

        try:
            if x_min and x_max:
                x_min = float(x_min)
                x_max = float(x_max)
                self.ax.set_xlim(x_min, x_max)
            if y_min and y_max:
                y_min = float(y_min)
                y_max = float(y_max)
                self.ax.set_ylim(y_min, y_max)
            if isinstance(self.ax, Axes3D):
                # Для 3D графика можно добавить ограничение Z, если нужно
                pass  # Здесь можно добавить поля для Z если требуется

            self.canvas.draw()
            self.update_status("Ограничения осей применены.")
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось применить ограничения осей:\n{e}")
            self.update_status("Ошибка при применении ограничений осей.")

    def update_plot(self):
        if self.z is None:
            return

        # Удаляем существующую colorbar, если она есть
        if self.colorbar:
            try:
                self.colorbar.remove()
            except:
                pass
            self.colorbar = None

        # Очищаем всю фигуру перед построением нового графика
        self.figure.clf()

        # Применяем выбранный стиль
        plt.style.use(self.style.get())

        # Строим контурную карту
        self.plot_contour()

        # Обновляем холст
        self.canvas.draw()
        self.update_status("График обновлён.")

    def save_plot(self):
        if self.z is None:
            messagebox.showwarning("Предупреждение", "Нет графика для сохранения.")
            return

        file_path = filedialog.asksaveasfilename(
            defaultextension=".png",
            filetypes=(
                ("PNG Image", "*.png"),
                ("JPEG Image", "*.jpg"),
                ("PDF Document", "*.pdf"),
                ("SVG Image", "*.svg"),
                ("All Files", "*.*")
            )
        )
        if not file_path:
            return

        try:
            self.figure.savefig(file_path)
            messagebox.showinfo("Успех", f"Изображение сохранено по пути:\n{file_path}")
            self.update_status(f"Изображение сохранено: {file_path}")
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось сохранить изображение:\n{e}")
            self.update_status("Ошибка при сохранении изображения.")

    def reset_view(self):
        if self.z is None:
            return
        # Очистка всей фигуры и повторное построение графика
        self.figure.clf()
        self.colorbar = None
        self.update_plot()
        self.update_status("Масштаб сброшен.")

    def show_about(self):
        messagebox.showinfo(
            "О программе",
            (
                "Приложение для отображения контурных карт.\n"
                "Разработано на Python с использованием Tkinter и Matplotlib.\n\n"
                "Функции:\n"
                "- Загрузка данных из CSV, TXT и Excel файлов.\n"
                "- Выбор типа отображения: заполненные контуры, контурные линии, 3D поверхность.\n"
                "- Настройка цветовой карты, уровней контуров и нормализации.\n"
                "- Настройка заголовка и меток осей.\n"
                "- Отображение статистики данных.\n"
                "- Включение/отключение сетки.\n"
                "- Применение логарифмической шкалы к значениям Z.\n"
                "- Настройка ограничений осей X и Y.\n"
                "- Интерполяция данных.\n"
                "- Сглаживание данных.\n"
                "- Интерактивные подсказки при наведении курсора.\n"
                "- Экспорт выбранных данных.\n"
                "- Сохранение графика в различных форматах.\n"
                "- Выбор стиля графика."
            )
        )

    def update_style(self):
        selected_style = self.style.get()
        plt.style.use(selected_style)
        self.update_plot()
        self.update_status(f"Стиль графика изменён на: {selected_style}")

    def apply_transformations(self):
        # Применяем логарифмическую шкалу и сглаживание при загрузке данных
        if self.log_scale.get():
            Z = self.z_original.copy()
            if np.any(Z <= 0):
                min_positive = np.min(Z[Z > 0])
                Z[Z <= 0] = min_positive
            Z = np.log(Z)
            self.z = Z
        else:
            self.z = self.z_original.copy()

        if self.smooth_var.get():
            sigma = self.smooth_sigma.get()
            self.z = gaussian_filter(self.z, sigma=sigma)

    def update_status(self, message):
        self.status.set(message)
        self.master.update_idletasks()

    def export_selected_region(self):
        # Функция для экспорта выбранной области данных
        # Реализуется с использованием функционала Matplotlib для выбора области
        if self.z is None:
            messagebox.showwarning("Предупреждение", "Сначала загрузите данные.")
            return

        messagebox.showinfo("Информация", "Пожалуйста, выделите область на графике для экспорта.")

        try:
            from matplotlib.widgets import RectangleSelector

            def onselect(eclick, erelease):
                try:
                    x1, y1 = int(eclick.xdata), int(eclick.ydata)
                    x2, y2 = int(erelease.xdata), int(erelease.ydata)
                    xmin, xmax = sorted([x1, x2])
                    ymin, ymax = sorted([y1, y2])
                    # Проверка границ
                    xmin = max(xmin, 0)
                    xmax = min(xmax, self.x - 1)
                    ymin = max(ymin, 0)
                    ymax = min(ymax, self.y - 1)
                    region = self.z[ymin:ymax+1, xmin:xmax+1]
                    # Сохранение выбранной области
                    export_path = filedialog.asksaveasfilename(
                        defaultextension=".csv",
                        filetypes=(("CSV файлы", "*.csv"), ("Все файлы", "*.*")),
                        title="Сохранить выбранную область данных"
                    )
                    if export_path:
                        pd.DataFrame(region).to_csv(export_path, header=False, index=False)
                        messagebox.showinfo("Успех", f"Данные сохранены по пути:\n{export_path}")
                        self.update_status(f"Данные экспортированы: {export_path}")
                    else:
                        self.update_status("Экспорт данных отменён.")
                except Exception as e:
                    messagebox.showerror("Ошибка", f"Не удалось экспортировать выбранную область:\n{e}")
                    self.update_status("Ошибка при экспорте данных.")

            rs = RectangleSelector(self.ax, onselect, drawtype='box', useblit=True,
                                   button=[1],  # Только левая кнопка мыши
                                   minspanx=5, minspany=5,
                                   spancoords='pixels',
                                   interactive=True)
            self.update_status("Выделите область для экспорта.")
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось экспортировать выбранную область:\n{e}")
            self.update_status("Ошибка при экспорте данных.")

def main():
    root = tk.Tk()
    app = ContourMapApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()