import { COUNTRIES, SECTORS } from "../data/market-data.js";

import { parseParamsFromElement } from "../utils/helper.js";

class Heatmap {
    constructor(options = {}) {
        this._userOptions = options;
        this._sendPostRequest = options.sendPostRequest || (typeof sendPostRequest !== "undefined" ? sendPostRequest : null);
        this._apiEndpoint = options.apiEndpoint || (typeof API_ENDPOINT !== "undefined" ? API_ENDPOINT : "");
        this._initCustomSelects = options.initCustomSelects || (typeof initCustomSelects !== "undefined" ? initCustomSelects : null);
        this._scrollToCenter = options.scrollToCenter || (typeof scrollToCenter !== "undefined" ? scrollToCenter : null);
        this._addThousandSeparator = options.addThousandSeparator || (typeof addThousandSeparator !== "undefined" ? addThousandSeparator : v => v);
        this._baseUrl = options.baseUrl || (typeof BASE_URL !== "undefined" ? BASE_URL : "/");
        this.iconPath = options.iconPath || (typeof ICON_PATH !== "undefined" ? ICON_PATH : "");
        this.iconType = options.iconType || (typeof ICON_TYPE !== "undefined" ? ICON_TYPE : ".svg");
        this.CONFIG = {
            TYPE: "coin",
            COUNTRY: "US",
            INDICES: null,
            SECTOR: null,
            IS_PRIMARY: true,
            SORT_VALUE: "market_cap",
            SORT_FIELD: "perf.market_cap",
            SORT_ORDER: "desc",
            SIZE_LABEL: "Market Cap",
            PRICE_LABEL: "Price",
            COLOR_FIELD: "active.chp",
            PERIOD: "1d",
            COLOR_LABEL: "Change D, %",
            COLOR_THRESHOLDS: [ -13, -8.7, -4.3, 0, 4.3, 8.7, 13 ],
            PER_PAGE: 100,
            SHOW_LOGO: true,
            DISPLAY_NAME: "name",
            DISPLAY_VALUE: "selected",
            COLOR_MODE: "fixed",
            BOX_GAP: 2,
            BOX_BORDER_RADIUS: 0,
            BOX_BORDER_WIDTH: 0,
            BOX_BORDER_COLOR: "rgba(0,0,0,0)",
            HOVER_BORDER_WIDTH: 2,
            HOVER_BORDER_COLOR: null,
            FONT_FAMILY: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
            SYMBOL_FONT_SIZE: 18,
            VALUE_FONT_SIZE: 18,
            MONO_FONT_SIZE: 15,
            MIN_FONT_SIZE: 10,
            FONT_SIZE_SCALE_FACTOR: .12,
            TEXT_FILL_COLOR: "rgba(255, 255, 255, 0.9)",
            TEXT_SHADOW_COLOR: "rgba(0, 0, 0, 0.5)",
            TEXT_SHADOW_BLUR: 2,
            LOGO_SIZE_MIN: 24,
            LOGO_SIZE_MAX: 50,
            LOGO_SIZE_RATIO: .35,
            MIN_BOX_SIZE: 35,
            ENABLE_WHEEL_ZOOM: false,
            MIN_ZOOM: 1,
            MAX_ZOOM: 14,
            ZOOM_STEP: .25,
            WHEEL_ZOOM_STEP: .15,
            ENABLE_HOVER_TOOLTIP: true,
            ENABLE_CLICK_TOOLTIP: true,
            LINK_TARGET: "_self",
            OVERVIEW_URL: "BASE_URL/symbols/{{symbol}}?exchange={{exchange}}",
            CHART_URL: "BASE_PATH/chart/?symbol={{ticker}}",
            AUTO_UPDATE: true,
            AUTO_UPDATE_INTERVAL: 6e4,
            OBSERVER_THRESHOLD: .1,
            OBSERVER_ROOT_MARGIN: "100px",
            ID_PREFIX: "",
            ENABLE_STORAGE: true,
            STORAGE_KEY: null,
            ENABLE_TYPE_SELECT: true,
            ENABLE_SORT_SELECT: true,
            ENABLE_COLOR_SELECT: true,
            ENABLE_PER_PAGE_SELECT: true,
            ENABLE_COUNTRY_SELECT: true,
            ENABLE_INDICES_SELECT: true,
            ENABLE_SECTOR_SELECT: true,
            ENABLE_PRIMARY_TOGGLE: true,
            ENABLE_SETTINGS: true,
            ENABLE_SCREENSHOT: true,
            ENABLE_FULLSCREEN: true,
            ENABLE_ZOOM_CONTROLS: true,
            ENABLE_LEGEND: true,
            ENABLE_OVERVIEW_LINK: true,
            ENABLE_CHART_LINK: true,
            colorClasses: {
                negative: [ "heatmap-negative-5", "heatmap-negative-4", "heatmap-negative-2" ],
                neutral: "heatmap-neutral-2",
                positive: [ "heatmap-positive-2", "heatmap-positive-4", "heatmap-positive-5" ]
            },
            colors: null,
            ...options
        };
        this.root = options.root || document;
        this.hostEl = options.hostElement || document.documentElement;
        this.state = {
            data: [],
            layout: [],
            isLoading: false,
            isFirstLoad: true,
            hoveredItem: null,
            clickedItem: null,
            minPerformance: -38,
            maxPerformance: 38,
            zoomLevel: 1,
            panX: 0,
            panY: 0,
            isPanning: false,
            lastMouseX: 0,
            lastMouseY: 0,
            autoUpdateTimer: null,
            availableSectors: null,
            indicesName: options.INDICES_NAME || null,
            indicesLogo: options.INDICES_LOGO || null,
            typeParams: {},
            sortParams: {},
            colorParams: {}
        };
        this.canvas = null;
        this.ctx = null;
        this.baseWidth = 0;
        this.baseHeight = 0;
        this.dpr = window.devicePixelRatio || 1;
        this.imageCache = new Map;
        this.els = {
            container: null,
            canvasWrapper: null,
            fullscreenWrapper: null,
            tooltip: null,
            clickTooltip: null,
            clickTooltipClose: null,
            loading: null,
            emptyState: null,
            legendBoxes: null,
            typeSelect: null,
            sortSelect: null,
            colorSelect: null,
            perPageSelect: null,
            countrySelect: null,
            indicesSelect: null,
            sectorSelect: null,
            settings: null,
            primaryToggle: null,
            screenshotBtn: null,
            fullscreenBtn: null,
            zoomInBtn: null,
            zoomOutBtn: null,
            zoomResetBtn: null,
            typeDropdown: null,
            sortDropdown: null,
            colorDropdown: null,
            countryDropdown: null,
            indicesDropdown: null,
            sectorDropdown: null
        };
        this.observer = null;
        this.#init();
    }
    #getEl(id) {
        return this.root.getElementById(this.CONFIG.ID_PREFIX + id);
    }
    async #init() {
        if (!this.CONFIG.STORAGE_KEY) {
            const typePrefix = this.CONFIG.TYPE || "crypto";
            this.CONFIG.STORAGE_KEY = `${typePrefix}_heatmap_prefs`;
        }
        this.#cacheElements();
        this.#applyVisibilityConfig();
        this.#extractColorsFromCSS();
        this.#createCanvas();
        this.#loadSavedPreferences();
        if (this.els.tooltip) {
            const priceLabel = this.els.tooltip.querySelector(".tooltip-price-label")?.textContent?.trim();
            if (priceLabel) this.CONFIG.PRICE_LABEL = priceLabel;
        }
        await this.#waitForLang();
        this.#initSelectors();
        this.#bindEvents();
        this.#setupObserver();
    }
    #waitForLang() {
        return new Promise(resolve => {
            if (window.LANG_LOADED) {
                resolve();
                return;
            }
            window.addEventListener("langLoaded", () => resolve(), {
                once: true
            });
            setTimeout(resolve, 500);
        });
    }
    #setupObserver() {
        if (!this.els.container) return;
        this.observer = new IntersectionObserver(entries => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    this.#loadData();
                    this.observer.disconnect();
                }
            });
        }, {
            threshold: this.CONFIG.OBSERVER_THRESHOLD,
            rootMargin: this.CONFIG.OBSERVER_ROOT_MARGIN
        });
        this.observer.observe(this.els.container);
    }
    #extractColorsFromCSS() {
        const bgColor = getComputedStyle(this.hostEl).getPropertyValue("--bg-secondary").trim() || "#181818";
        const canvas = document.createElement("canvas");
        canvas.width = 10;
        canvas.height = 10;
        const ctx = canvas.getContext("2d", {
            willReadFrequently: true
        });
        const temp = document.createElement("div");
        temp.style.cssText = "position:absolute;visibility:hidden;pointer-events:none";
        const tempParent = this.els.container || document.body;
        tempParent.appendChild(temp);
        const getColorFromClass = className => {
            ctx.fillStyle = bgColor;
            ctx.fillRect(0, 0, 10, 10);
            temp.className = className;
            const computed = getComputedStyle(temp).backgroundColor;
            ctx.fillStyle = computed;
            ctx.fillRect(0, 0, 10, 10);
            const pixel = ctx.getImageData(5, 5, 1, 1).data;
            return [ pixel[0], pixel[1], pixel[2] ];
        };
        const classes = this.CONFIG.colorClasses;
        this.CONFIG.colors = {
            negative: classes.negative.map(cls => getColorFromClass(cls)),
            neutral: getColorFromClass(classes.neutral),
            positive: classes.positive.map(cls => getColorFromClass(cls))
        };
        tempParent.removeChild(temp);
    }
    #cacheElements() {
        this.els.container = this.#getEl("heatmapContainer");
        this.els.canvasWrapper = this.#getEl("heatmapTreemap");
        this.els.fullscreenWrapper = this.#getEl("heatmapFullscreenWrapper");
        this.els.tooltip = this.#getEl("heatmapTooltip");
        this.els.clickTooltip = this.#getEl("heatmapClickTooltip");
        this.els.clickTooltipClose = this.#getEl("heatmapClickTooltipClose");
        this.els.loading = this.#getEl("heatmapLoading");
        this.els.skeleton = this.#getEl("heatmapSkeleton");
        this.els.emptyState = this.#getEl("heatmapEmptyState");
        this.els.legendBoxes = this.#getEl("heatmapLegendBoxes");
        this.els.typeSelect = this.#getEl("heatmapTypeSelect");
        this.els.sortSelect = this.#getEl("heatmapSortSelect");
        this.els.colorSelect = this.#getEl("heatmapColorSelect");
        this.els.perPageSelect = this.#getEl("heatmapPerPageSelect");
        this.els.countrySelect = this.#getEl("heatmapCountrySelect");
        this.els.indicesSelect = this.#getEl("heatmapIndicesSelect");
        this.els.sectorSelect = this.#getEl("heatmapSectorSelect");
        this.els.settings = this.#getEl("heatmapSettings");
        this.els.primaryToggle = this.#getEl("heatmapPrimaryToggle");
        this.els.screenshotBtn = this.#getEl("heatmapScreenshotBtn");
        this.els.fullscreenBtn = this.#getEl("heatmapFullscreenBtn");
        this.els.zoomInBtn = this.#getEl("heatmapZoomInBtn");
        this.els.zoomOutBtn = this.#getEl("heatmapZoomOutBtn");
        this.els.zoomResetBtn = this.#getEl("heatmapZoomResetBtn");
        const container = this.els.container;
        this.els.overviewLink = container?.querySelector(".overview-link") ?? null;
        this.els.chartLink = container?.querySelector(".chart-link") ?? null;
        this.els.typeDropdown = this.#getDropdown(this.els.typeSelect);
        this.els.sortDropdown = this.#getDropdown(this.els.sortSelect);
        this.els.colorDropdown = this.#getDropdown(this.els.colorSelect);
        this.els.countryDropdown = this.#getDropdown(this.els.countrySelect);
        this.els.indicesDropdown = this.#getDropdown(this.els.indicesSelect);
        this.els.sectorDropdown = this.#getDropdown(this.els.sectorSelect);
    }
    #applyVisibilityConfig() {
        const hide = el => {
            if (el) el.style.display = "none";
        };
        const show = el => {
            if (el) el.style.display = "";
        };
        const apply = (enabled, el) => enabled ? show(el) : hide(el);
        const C = this.CONFIG;
        const e = this.els;
        apply(C.ENABLE_TYPE_SELECT, e.typeSelect);
        apply(C.ENABLE_SORT_SELECT, e.sortSelect);
        apply(C.ENABLE_COLOR_SELECT, e.colorSelect);
        apply(C.ENABLE_PER_PAGE_SELECT, e.perPageSelect);
        apply(C.ENABLE_COUNTRY_SELECT, e.countrySelect);
        apply(C.ENABLE_INDICES_SELECT, e.indicesSelect);
        apply(C.ENABLE_SECTOR_SELECT, e.sectorSelect);
        apply(C.ENABLE_PRIMARY_TOGGLE, e.primaryToggle);
        apply(C.ENABLE_SETTINGS, e.settings);
        apply(C.ENABLE_SCREENSHOT, e.screenshotBtn);
        apply(C.ENABLE_FULLSCREEN, e.fullscreenBtn);
        apply(C.ENABLE_LEGEND, e.legendBoxes);
        apply(C.ENABLE_ZOOM_CONTROLS, e.zoomInBtn);
        apply(C.ENABLE_ZOOM_CONTROLS, e.zoomOutBtn);
        apply(C.ENABLE_ZOOM_CONTROLS, e.zoomResetBtn);
        apply(C.ENABLE_OVERVIEW_LINK, e.overviewLink);
        apply(C.ENABLE_CHART_LINK, e.chartLink);
    }
    #getDropdown(select) {
        if (!select) return null;
        const dropdownId = select.getAttribute("data-dropdown-id");
        return dropdownId ? this.#getEl(dropdownId) : select;
    }
    #createCanvas() {
        if (!this.els.canvasWrapper) return;
        this.canvas = document.createElement("canvas");
        this.canvas.id = this.CONFIG.ID_PREFIX + "heatmapCanvas";
        this.canvas.style.cssText = "display: block; width: 100%; height: 100%; cursor: grab;";
        this.els.canvasWrapper.appendChild(this.canvas);
        this.ctx = this.canvas.getContext("2d", {
            willReadFrequently: true
        });
        requestAnimationFrame(() => this.#resizeCanvas());
    }
    #resizeCanvas() {
        if (!this.canvas || !this.els.canvasWrapper) return;
        const rect = this.els.canvasWrapper.getBoundingClientRect();
        const width = rect.width;
        const height = rect.height || 600;
        const dpr = this.dpr;
        const canvas = this.canvas;
        const ctx = this.ctx;
        this.baseWidth = width;
        this.baseHeight = height;
        canvas.width = width * dpr;
        canvas.height = height * dpr;
        canvas.style.cssText = `width:${width}px;height:${height}px`;
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    }
    #loadSavedPreferences() {
        this.#readDefaultsFromDOM();
        this.#updateSettingsDOMFromState();
        if (!this.CONFIG.ENABLE_STORAGE) return;
        const saved = STORAGE.get(this.CONFIG.STORAGE_KEY);
        if (saved) {
            if (saved.type) this.CONFIG.TYPE = saved.type;
            if (saved.country) this.CONFIG.COUNTRY = saved.country;
            if (saved.indices) {
                this.CONFIG.INDICES = saved.indices;
                this.state.indicesName = saved.indicesName || null;
                this.state.indicesLogo = saved.indicesLogo || null;
            }
            if (saved.sector) this.CONFIG.SECTOR = saved.sector;
            if (saved.availableSectors) this.state.availableSectors = saved.availableSectors;
            if (typeof saved.isPrimary === "boolean") this.CONFIG.IS_PRIMARY = saved.isPrimary;
            if (saved.sortValue) {
                this.CONFIG.SORT_VALUE = saved.sortValue;
                const sortOpt = this.els.sortDropdown?.querySelector(`.select-option[data-value="${saved.sortValue}"]`);
                if (sortOpt) {
                    if (saved.sortValue === "mono") {
                        const firstNonMono = this.els.sortDropdown.querySelector('.select-option:not([data-value="mono"])');
                        this.CONFIG.SIZE_LABEL = firstNonMono ? firstNonMono.textContent.trim() : sortOpt.textContent.trim();
                    } else {
                        this.CONFIG.SIZE_LABEL = sortOpt.textContent.trim();
                    }
                }
            }
            if (saved.colorParams) {
                const expectedParam = `${saved.colorParams.columns || ""},${saved.colorParams.period || ""}`;
                const colorOpt = this.els.colorDropdown?.querySelector(`.select-option[data-param-value="${expectedParam}"]`);
                if (colorOpt) {
                    this.CONFIG.COLOR_LABEL = colorOpt.textContent.trim();
                }
            }
            if (saved.perPage) this.CONFIG.PER_PAGE = saved.perPage;
            if (saved.typeParams) this.state.typeParams = saved.typeParams;
            if (saved.sortParams) this.state.sortParams = saved.sortParams;
            if (saved.colorParams) this.state.colorParams = saved.colorParams;
            if (typeof saved.showLogo === "boolean") this.CONFIG.SHOW_LOGO = saved.showLogo;
            if (saved.displayName) this.CONFIG.DISPLAY_NAME = saved.displayName;
            if (saved.displayValue) this.CONFIG.DISPLAY_VALUE = saved.displayValue;
            if (saved.colorMode) this.CONFIG.COLOR_MODE = saved.colorMode;
            if (typeof saved.borderRadius === "number") this.CONFIG.BOX_BORDER_RADIUS = saved.borderRadius;
            this.#updateDOMFromState();
        }
    }
    #readDefaultsFromDOM() {
        if (this.els.typeSelect) {
            const selectedType = this.els.typeDropdown.querySelector(".select-option.selected");
            if (selectedType) {
                this.CONFIG.TYPE = selectedType.dataset.value;
                this.state.typeParams = parseParamsFromElement(selectedType);
            }
        } else {
            this.state.typeParams = {
                type: this.CONFIG.TYPE
            };
        }
        if (this.els.sortSelect) {
            const selectedSort = this.els.sortDropdown.querySelector(".select-option.selected");
            if (selectedSort) {
                this.CONFIG.SORT_VALUE = selectedSort.dataset.value;
                this.state.sortParams = parseParamsFromElement(selectedSort);
                if (selectedSort.dataset.value === "mono") {
                    const firstNonMono = this.els.sortDropdown.querySelector('.select-option:not([data-value="mono"])');
                    this.CONFIG.SIZE_LABEL = firstNonMono ? firstNonMono.textContent.trim() : selectedSort.textContent.trim();
                } else {
                    this.CONFIG.SIZE_LABEL = selectedSort.textContent.trim();
                }
            }
        } else {
            const isStock = this.CONFIG.TYPE === "stock";
            const defaultSortColumn = isStock ? "earnings.market_cap" : "perf.market_cap";
            this.state.sortParams = {
                columns: this.CONFIG.SORT_FIELD || defaultSortColumn,
                sort_by: (this.CONFIG.SORT_FIELD || defaultSortColumn) + "_" + (this.CONFIG.SORT_ORDER || "desc")
            };
        }
        if (this.els.colorSelect) {
            const selectedColor = this.els.colorDropdown.querySelector(".select-option.selected");
            if (selectedColor) {
                this.CONFIG.COLOR_LABEL = selectedColor.textContent.trim() || "Performance";
                const rangeStr = selectedColor.dataset.range || "-38,-25.3,-12.7,0,12.7,25.3,38";
                this.CONFIG.COLOR_THRESHOLDS = rangeStr.split(",").map(v => parseFloat(v.trim()));
                this.state.colorParams = parseParamsFromElement(selectedColor);
            }
        } else {
            this.state.colorParams = {
                columns: this.CONFIG.COLOR_FIELD || "active.chp",
                period: this.CONFIG.PERIOD || "1d"
            };
        }
        if (this.els.perPageSelect) {
            const selectedPerPage = this.els.perPageSelect.querySelector(".select-option.selected");
            if (selectedPerPage) {
                this.CONFIG.PER_PAGE = parseInt(selectedPerPage.dataset.value);
            }
        }
        if (this.els.countrySelect) {
            const selectedCountry = this.els.countrySelect.querySelector(".select-option.selected");
            if (selectedCountry) {
                this.CONFIG.COUNTRY = selectedCountry.dataset.value;
            }
        }
        if (this.els.sectorSelect) {
            const selectedSector = this.els.sectorSelect.querySelector(".select-option.selected");
            if (selectedSector) {
                const sectorValue = selectedSector.dataset.value;
                this.CONFIG.SECTOR = sectorValue === "all" ? null : sectorValue;
            }
        }
    }
    #updateDOMFromState() {
        this.#updateSelectDOM("heatmapTypeSelect", this.CONFIG.TYPE);
        this.#updateSortSelectDOM();
        this.#updateColorSelectDOM();
        this.#updateSelectDOM("heatmapPerPageSelect", this.CONFIG.PER_PAGE.toString());
        this.#updateCountryDOMFromState();
        this.#updateIndicesDOMFromState();
        this.#updatePrimaryToggleDOMFromState();
        this.#updateSettingsDOMFromState();
    }
    #updatePrimaryToggleDOMFromState() {
        if (this.els.primaryToggle) {
            this.els.primaryToggle.checked = this.CONFIG.IS_PRIMARY;
        }
        this.#updatePrimaryIconVisibility();
    }
    #updatePrimaryIconVisibility() {
        if (this.els.countrySelect) {
            const primaryIcon = this.els.countrySelect.querySelector(".country-text-primary-icon");
            if (primaryIcon) {
                primaryIcon.classList.toggle("hidden", !this.CONFIG.IS_PRIMARY);
            }
        }
    }
    #updateCountryDOMFromState() {
        if (!this.els.countrySelect || !this.CONFIG.COUNTRY || !COUNTRIES[this.CONFIG.COUNTRY]) return;
        const countryData = COUNTRIES[this.CONFIG.COUNTRY];
        const trigger = this.els.countrySelect.querySelector(".select-trigger");
        if (trigger) {
            const triggerSpan = trigger.querySelector(".select-option-img");
            if (triggerSpan) {
                triggerSpan.innerHTML = `\n                    <span class="flag-icon-container">\n                        <img alt="${countryData.name}" class="flag-img" data-src="${this.iconPath}country/${this.CONFIG.COUNTRY}${this.iconType}">\n                    </span>\n                    <span class="select-trigger-text">${countryData.name}</span>\n                `;
            }
        }
        const options = this.els.countryDropdown.querySelectorAll(".select-option");
        options.forEach(opt => {
            if (opt.dataset.value === this.CONFIG.COUNTRY) {
                opt.classList.add("selected");
            } else {
                opt.classList.remove("selected");
            }
        });
    }
    #updateIndicesDOMFromState() {
        if (!this.els.indicesSelect || !this.CONFIG.INDICES || !this.state.indicesName) return;
        const triggerSpan = this.els.indicesSelect.querySelector(".select-option-img");
        if (triggerSpan) {
            const logoHtml = this.state.indicesLogo ? `<img alt="${this.state.indicesName}" class="flag-img" data-src="${this.state.indicesLogo}">` : `<svg width="100%" height="100%" fill="none" stroke="currentColor"><use href="#global"></use></svg>`;
            triggerSpan.innerHTML = `\n                <span class="flag-icon-container">${logoHtml}</span>\n                <span class="select-trigger-text">${this.state.indicesName}</span>\n            `;
        }
    }
    #updateSortSelectDOM() {
        if (!this.els.sortSelect) return;
        const options = this.els.sortDropdown.querySelectorAll(".select-option");
        const triggerText = this.els.sortSelect.querySelector(".select-trigger-text");
        options.forEach(opt => {
            if (opt.dataset.value === this.CONFIG.SORT_VALUE) {
                opt.classList.add("selected");
                if (triggerText) {
                    triggerText.textContent = opt.textContent.trim();
                }
            } else {
                opt.classList.remove("selected");
            }
        });
    }
    #updateSettingsDOMFromState() {
        if (!this.els.settings) return;
        const showLogoOpt = this.els.settings.querySelector('.select-option[data-value="show-logo"]');
        if (showLogoOpt) {
            if (this.CONFIG.SHOW_LOGO) {
                showLogoOpt.classList.add("selected");
            } else {
                showLogoOpt.classList.remove("selected");
            }
        }
        const displayNameOptions = this.els.settings.querySelectorAll(".display-name-options .select-option");
        displayNameOptions.forEach(opt => {
            const value = opt.dataset.value;
            const isSelected = value === "display-symbol" && this.CONFIG.DISPLAY_NAME === "symbol" || value === "display-name" && this.CONFIG.DISPLAY_NAME === "name" || value === "display-none" && this.CONFIG.DISPLAY_NAME === "none";
            if (isSelected) {
                opt.classList.add("selected");
            } else {
                opt.classList.remove("selected");
            }
        });
        const displayValueOptions = this.els.settings.querySelectorAll(".display-value-options .select-option");
        displayValueOptions.forEach(opt => {
            const value = opt.dataset.value;
            const isSelected = value === "value-price" && this.CONFIG.DISPLAY_VALUE === "price" || value === "value-marketcap" && this.CONFIG.DISPLAY_VALUE === "marketcap" || value === "value-selected" && this.CONFIG.DISPLAY_VALUE === "selected" || value === "value-none" && this.CONFIG.DISPLAY_VALUE === "none";
            if (isSelected) {
                opt.classList.add("selected");
            } else {
                opt.classList.remove("selected");
            }
            if (value === "value-marketcap") {
                opt.textContent = this.CONFIG.SIZE_LABEL || "Market Cap";
            } else if (value === "value-selected") {
                opt.textContent = this.CONFIG.COLOR_LABEL || "Selected metric";
            }
        });
        const colorModeOptions = this.els.settings.querySelectorAll(".color-mode-options .select-option");
        colorModeOptions.forEach(opt => {
            const isGradient = opt.dataset.value === "color-gradient";
            const isFixed = opt.dataset.value === "color-fixed";
            if (isGradient && this.CONFIG.COLOR_MODE === "gradient" || isFixed && this.CONFIG.COLOR_MODE === "fixed") {
                opt.classList.add("selected");
            } else {
                opt.classList.remove("selected");
            }
        });
        const borderRadiusOpt = this.els.settings.querySelector('.select-option[data-value="border-radius"]');
        if (borderRadiusOpt) {
            if (this.CONFIG.BOX_BORDER_RADIUS > 0) {
                borderRadiusOpt.classList.add("selected");
            } else {
                borderRadiusOpt.classList.remove("selected");
            }
        }
    }
    #updateSelectDOM(selectId, value) {
        const selectMap = {
            "heatmapTypeSelect": {
                select: this.els.typeSelect,
                dropdown: this.els.typeDropdown
            },
            "heatmapPerPageSelect": {
                select: this.els.perPageSelect,
                dropdown: this.els.perPageSelect
            }
        };
        const cached = selectMap[selectId];
        if (!cached?.select) return;
        const {select: select, dropdown: dropdown} = cached;
        const options = dropdown.querySelectorAll(".select-option");
        const triggerText = select.querySelector(".select-trigger-text");
        options.forEach(opt => {
            if (opt.dataset.value === value) {
                opt.classList.add("selected");
                if (triggerText) {
                    triggerText.textContent = opt.textContent.trim();
                }
            } else {
                opt.classList.remove("selected");
            }
        });
    }
    #updateColorSelectDOM() {
        if (!this.els.colorSelect) return;
        const options = this.els.colorDropdown.querySelectorAll(".select-option");
        const triggerText = this.els.colorSelect.querySelector(".select-trigger-text");
        const colorColumns = this.state.colorParams?.columns || "";
        const colorPeriod = this.state.colorParams?.period || "";
        const expectedParamValue = `${colorColumns},${colorPeriod}`;
        options.forEach(opt => {
            const optParamValue = opt.dataset.paramValue || "";
            if (optParamValue === expectedParamValue) {
                opt.classList.add("selected");
                if (triggerText) {
                    triggerText.textContent = opt.textContent.trim();
                }
            } else {
                opt.classList.remove("selected");
            }
        });
    }
    #populateSectorOptions() {
        if (!this.els.sectorSelect || !this.els.sectorDropdown) return;
        const optionsList = this.els.sectorDropdown.querySelector(".select-options");
        if (!optionsList) return;
        optionsList.innerHTML = "";
        const allOption = document.createElement("li");
        allOption.className = "select-option" + (this.CONFIG.SECTOR === null ? " selected" : "");
        allOption.dataset.value = "all";
        allOption.dataset.paramName = "sector";
        allOption.dataset.paramValue = "";
        allOption.textContent = SECTORS["all"] || "All Sectors";
        optionsList.appendChild(allOption);
        if (this.state.availableSectors && this.state.availableSectors.length > 0) {
            this.state.availableSectors.forEach(sectorName => {
                const option = document.createElement("li");
                option.className = "select-option" + (this.CONFIG.SECTOR === sectorName ? " selected" : "");
                option.dataset.value = sectorName;
                option.dataset.paramName = "sector";
                option.dataset.paramValue = sectorName;
                option.textContent = SECTORS[sectorName] || sectorName;
                optionsList.appendChild(option);
            });
        }
        const triggerText = this.els.sectorSelect.querySelector(".select-trigger-text");
        if (triggerText) {
            const currentSector = this.CONFIG.SECTOR;
            triggerText.textContent = currentSector ? SECTORS[currentSector] || currentSector : SECTORS["all"] || "All Sectors";
        }
    }
    #populateCountryOptions() {
        if (!this.els.countrySelect || !this.els.countryDropdown) return;
        const dropdown = this.els.countryDropdown;
        const optionsList = dropdown ? dropdown.querySelector(".select-options") : null;
        if (!optionsList) return;
        optionsList.innerHTML = "";
        Object.entries(COUNTRIES).forEach(([code, data]) => {
            const option = document.createElement("li");
            option.className = "select-option" + (this.CONFIG.COUNTRY === code ? " selected" : "");
            option.dataset.value = code;
            option.dataset.paramName = "country";
            option.dataset.paramValue = code;
            option.innerHTML = `\n                <span class="select-option-img">\n                    <span class="flag-icon-container">\n                        <img alt="${data.name}" class="flag-img" data-src="${this.iconPath}country/${code}${this.iconType}">\n                    </span>\n                    <span class="country-name">${data.name}</span>\n                </span>\n            `;
            optionsList.appendChild(option);
        });
        const trigger = this.els.countrySelect.querySelector(".select-trigger");
        if (trigger && this.CONFIG.COUNTRY && COUNTRIES[this.CONFIG.COUNTRY]) {
            const countryData = COUNTRIES[this.CONFIG.COUNTRY];
            const triggerSpan = trigger.querySelector(".select-option-img");
            if (triggerSpan) {
                triggerSpan.innerHTML = `\n                    <span class="flag-icon-container">\n                        <img alt="${countryData.name}" class="flag-img" data-src="${this.iconPath}country/${this.CONFIG.COUNTRY}${this.iconType}">\n                    </span>\n                    <span class="select-trigger-text">${countryData.name}</span>\n                `;
            }
        }
    }
    #indicesLoaded=false;
    async #loadIndicesList() {
        if (!this.els.indicesSelect || this.#indicesLoaded) return;
        if (!this.els.indicesDropdown) return;
        try {
            const response = await this._sendPostRequest(`${this._apiEndpoint}stock/indices`, {
                filters: JSON.stringify({
                    "profile.stocks_count_gt": 10
                }),
                country: this.CONFIG.COUNTRY,
                columns: "profile.name,profile.self_logo"
            });
            const loadingSpinner = this.els.indicesDropdown.querySelector(".menu-search-result-initial-loading");
            const optionsList = this.els.indicesDropdown.querySelector(".select-options");
            if (response?.status && response.response) {
                this.#indicesLoaded = true;
                if (loadingSpinner) loadingSpinner.classList.add("d-none");
                if (optionsList) {
                    optionsList.classList.remove("d-none");
                    this.#buildIndicesOptions(optionsList, response.response);
                }
            } else {
                this.#indicesLoaded = true;
                if (loadingSpinner) loadingSpinner.classList.add("d-none");
                if (optionsList) {
                    optionsList.classList.remove("d-none");
                    optionsList.innerHTML = '<li class="select-option disabled no-result"><span class="select-option-img">No indices available</span></li>';
                }
            }
        } catch (error) {
            console.error("Failed to load indices:", error);
            const loadingSpinner = this.els.indicesDropdown.querySelector(".menu-search-result-initial-loading");
            const optionsList = this.els.indicesDropdown.querySelector(".select-options");
            if (loadingSpinner) loadingSpinner.classList.add("d-none");
            if (optionsList) {
                optionsList.classList.remove("d-none");
                optionsList.innerHTML = '<li class="select-option disabled no-result"><span class="select-option-img">Failed to load indices</span></li>';
            }
        }
    }
    #buildIndicesOptions(optionsList, indices) {
        optionsList.innerHTML = "";
        const allOption = document.createElement("li");
        allOption.className = "select-option" + (this.CONFIG.INDICES === null ? " selected" : "");
        allOption.dataset.value = "all";
        allOption.dataset.paramName = "indices";
        allOption.dataset.paramValue = "";
        allOption.innerHTML = `\n            <span class="select-option-img">\n                <div class="flag-icon-container">\n                    <svg width="100%" height="100%" fill="none" stroke="currentColor">\n                        <use href="#global"></use>\n                    </svg>\n                </div>\n                All Index\n            </span>\n        `;
        optionsList.appendChild(allOption);
        indices.forEach(item => {
            const ticker = item.ticker;
            const name = item.profile?.name || item.profile?.symbol || ticker;
            const selfLogo = item.profile?.self_logo || "";
            const option = document.createElement("li");
            option.className = "select-option" + (this.CONFIG.INDICES === ticker ? " selected" : "");
            option.dataset.value = ticker;
            option.dataset.paramName = "indices";
            option.dataset.paramValue = ticker;
            option.innerHTML = `\n                <span class="select-option-img">\n                    <span class="flag-icon-container">\n                        <img alt="${name}" class="flag-img" data-src="${this.iconPath}${selfLogo}${this.iconType}">\n                    </span>\n                    <span class="index-name">${name}</span>\n                </span>\n            `;
            optionsList.appendChild(option);
        });
        if (this.CONFIG.INDICES && this.els.indicesSelect) {
            const selectedIndex = indices.find(i => i.ticker === this.CONFIG.INDICES);
            if (selectedIndex) {
                const triggerSpan = this.els.indicesSelect.querySelector(".select-option-img");
                if (triggerSpan) {
                    const name = selectedIndex.profile?.name || selectedIndex.profile?.symbol || this.CONFIG.INDICES;
                    const selfLogo = selectedIndex.profile?.self_logo || "";
                    triggerSpan.innerHTML = `\n                        <span class="flag-icon-container">\n                            <img alt="${name}" class="flag-img" data-src="${this.iconPath}${selfLogo}${this.iconType}">\n                        </span>\n                        <span class="select-trigger-text">${name}</span>\n                    `;
                }
            }
        }
    }
    #resetIndicesDropdown() {
        if (!this.els.indicesSelect) return;
        this.state.indicesName = null;
        this.state.indicesLogo = null;
        const triggerSpan = this.els.indicesSelect.querySelector(".select-option-img");
        if (triggerSpan) {
            triggerSpan.innerHTML = `\n                <span class="flag-icon-container">\n                    <svg width="100%" height="100%" fill="none" stroke="currentColor">\n                        <use href="#global"></use>\n                    </svg>\n                </span>\n                <span class="select-trigger-text">All Index</span>\n            `;
        }
        if (!this.els.indicesDropdown) return;
        const loadingSpinner = this.els.indicesDropdown.querySelector(".menu-search-result-initial-loading");
        const optionsList = this.els.indicesDropdown.querySelector(".select-options");
        if (loadingSpinner) loadingSpinner.classList.remove("d-none");
        if (optionsList) {
            optionsList.classList.add("d-none");
            optionsList.innerHTML = "";
        }
    }
    #resetSectorDropdown() {
        if (!this.els.sectorSelect) return;
        const triggerText = this.els.sectorSelect.querySelector(".select-trigger-text");
        if (triggerText) {
            triggerText.textContent = SECTORS["all"] || "All Sectors";
        }
        if (!this.els.sectorDropdown) return;
        const options = this.els.sectorDropdown.querySelectorAll(".select-option");
        options.forEach(opt => {
            if (opt.dataset.value === "all") {
                opt.classList.add("selected");
            } else {
                opt.classList.remove("selected");
            }
        });
    }
    #savePreferences() {
        if (!this.CONFIG.ENABLE_STORAGE) return;
        STORAGE.set(this.CONFIG.STORAGE_KEY, {
            type: this.CONFIG.TYPE,
            country: this.CONFIG.COUNTRY,
            indices: this.CONFIG.INDICES,
            indicesName: this.state.indicesName || null,
            indicesLogo: this.state.indicesLogo || null,
            sector: this.CONFIG.SECTOR,
            availableSectors: this.state.availableSectors || null,
            isPrimary: this.CONFIG.IS_PRIMARY,
            sortValue: this.CONFIG.SORT_VALUE,
            sizeLabel: this.CONFIG.SIZE_LABEL,
            colorLabel: this.CONFIG.COLOR_LABEL,
            perPage: this.CONFIG.PER_PAGE,
            typeParams: this.state.typeParams,
            sortParams: this.state.sortParams,
            colorParams: this.state.colorParams,
            showLogo: this.CONFIG.SHOW_LOGO,
            displayName: this.CONFIG.DISPLAY_NAME,
            displayValue: this.CONFIG.DISPLAY_VALUE,
            colorMode: this.CONFIG.COLOR_MODE,
            borderRadius: this.CONFIG.BOX_BORDER_RADIUS
        });
    }
    #bindEvents() {
        let resizeTimeout;
        window.addEventListener("resize", () => {
            clearTimeout(resizeTimeout);
            resizeTimeout = setTimeout(() => {
                this.#resizeCanvas();
                if (this.state.data.length > 0) {
                    this.#calculateLayout();
                    this.#render();
                }
            }, 250);
        });
        document.addEventListener("visibilitychange", () => {
            if (document.hidden) {
                this.#stopAutoUpdate();
            } else {
                this.#startAutoUpdate();
            }
        });
        if (this.canvas) {
            this.canvas.addEventListener("wheel", e => {
                const isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement);
                if (!this.CONFIG.ENABLE_WHEEL_ZOOM && !isFullscreen) return;
                e.preventDefault();
                const rect = this.canvas.getBoundingClientRect();
                const mouseX = e.clientX - rect.left;
                const mouseY = e.clientY - rect.top;
                const delta = e.deltaY > 0 ? -this.CONFIG.WHEEL_ZOOM_STEP : this.CONFIG.WHEEL_ZOOM_STEP;
                const newZoom = Math.max(this.CONFIG.MIN_ZOOM, Math.min(this.CONFIG.MAX_ZOOM, this.state.zoomLevel + delta));
                if (newZoom !== this.state.zoomLevel) {
                    const centerX = this.baseWidth / 2;
                    const centerY = this.baseHeight / 2;
                    const mouseRelX = mouseX - centerX - this.state.panX;
                    const mouseRelY = mouseY - centerY - this.state.panY;
                    const zoomFactor = newZoom / this.state.zoomLevel;
                    this.state.panX -= mouseRelX * (zoomFactor - 1);
                    this.state.panY -= mouseRelY * (zoomFactor - 1);
                    this.state.zoomLevel = newZoom;
                    this.#constrainPan();
                    this.#render();
                    this.#updateCursor();
                }
            }, {
                passive: false
            });
            this.canvas.addEventListener("mousedown", e => {
                if (this.state.zoomLevel > 1) {
                    this.state.isPanning = true;
                    this.state.lastMouseX = e.clientX;
                    this.state.lastMouseY = e.clientY;
                    this.canvas.style.cursor = "grabbing";
                }
            });
            this.canvas.addEventListener("mousemove", e => {
                const rect = this.canvas.getBoundingClientRect();
                const mouseX = e.clientX - rect.left;
                const mouseY = e.clientY - rect.top;
                if (this.state.isPanning) {
                    const dx = e.clientX - this.state.lastMouseX;
                    const dy = e.clientY - this.state.lastMouseY;
                    this.state.panX += dx;
                    this.state.panY += dy;
                    this.state.lastMouseX = e.clientX;
                    this.state.lastMouseY = e.clientY;
                    this.#constrainPan();
                    this.#render();
                } else {
                    const item = this.#getItemAtPosition(mouseX, mouseY);
                    if (item) {
                        this.#showTooltip(e, item.data);
                    } else {
                        this.#hideTooltip();
                    }
                }
            });
            this.canvas.addEventListener("mouseup", () => {
                this.state.isPanning = false;
                this.#updateCursor();
            });
            this.canvas.addEventListener("mouseleave", () => {
                this.state.isPanning = false;
                this.#hideTooltip();
                this.#updateCursor();
            });
            let lastTouchX = 0;
            let lastTouchY = 0;
            let initialPinchDistance = 0;
            let isTouchPanning = false;
            this.canvas.addEventListener("touchstart", e => {
                if (e.touches.length === 1) {
                    if (this.state.zoomLevel > 1) {
                        isTouchPanning = true;
                        lastTouchX = e.touches[0].clientX;
                        lastTouchY = e.touches[0].clientY;
                    }
                } else if (e.touches.length === 2) {
                    isTouchPanning = false;
                    const dx = e.touches[0].clientX - e.touches[1].clientX;
                    const dy = e.touches[0].clientY - e.touches[1].clientY;
                    initialPinchDistance = Math.sqrt(dx * dx + dy * dy);
                }
            }, {
                passive: true
            });
            this.canvas.addEventListener("touchmove", e => {
                if (e.touches.length === 1 && isTouchPanning && this.state.zoomLevel > 1) {
                    e.preventDefault();
                    const dx = e.touches[0].clientX - lastTouchX;
                    const dy = e.touches[0].clientY - lastTouchY;
                    this.state.panX += dx;
                    this.state.panY += dy;
                    lastTouchX = e.touches[0].clientX;
                    lastTouchY = e.touches[0].clientY;
                    this.#constrainPan();
                    this.#render();
                } else if (e.touches.length === 2) {
                    e.preventDefault();
                    const dx = e.touches[0].clientX - e.touches[1].clientX;
                    const dy = e.touches[0].clientY - e.touches[1].clientY;
                    const currentDistance = Math.sqrt(dx * dx + dy * dy);
                    if (initialPinchDistance > 0) {
                        const scale = currentDistance / initialPinchDistance;
                        const newZoom = Math.min(Math.max(this.state.zoomLevel * scale, 1), this.CONFIG.MAX_ZOOM);
                        const centerX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
                        const centerY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
                        const rect = this.canvas.getBoundingClientRect();
                        const touchX = centerX - rect.left;
                        const touchY = centerY - rect.top;
                        const zoomFactor = newZoom / this.state.zoomLevel;
                        this.state.panX = touchX - (touchX - this.state.panX) * zoomFactor;
                        this.state.panY = touchY - (touchY - this.state.panY) * zoomFactor;
                        this.state.zoomLevel = newZoom;
                        this.#constrainPan();
                        this.#render();
                        this.#updateResetButtonVisibility();
                    }
                    initialPinchDistance = currentDistance;
                }
            }, {
                passive: false
            });
            this.canvas.addEventListener("touchend", () => {
                isTouchPanning = false;
                initialPinchDistance = 0;
                this.#updateResetButtonVisibility();
            });
            let hasDragged = false;
            let dragStartX = 0;
            let dragStartY = 0;
            this.canvas.addEventListener("mousedown", e => {
                hasDragged = false;
                dragStartX = e.clientX;
                dragStartY = e.clientY;
            });
            this.canvas.addEventListener("mousemove", e => {
                if (Math.abs(e.clientX - dragStartX) > 5 || Math.abs(e.clientY - dragStartY) > 5) {
                    hasDragged = true;
                }
            });
            this.canvas.addEventListener("click", e => {
                if (hasDragged) {
                    hasDragged = false;
                    return;
                }
                const rect = this.canvas.getBoundingClientRect();
                const mouseX = e.clientX - rect.left;
                const mouseY = e.clientY - rect.top;
                const item = this.#getItemAtPosition(mouseX, mouseY);
                if (item) {
                    this.#showClickTooltip(e, item.data);
                } else {
                    this.#hideClickTooltip();
                }
            });
        }
        if (this.els.screenshotBtn) {
            this.els.screenshotBtn.addEventListener("click", () => this.#takeScreenshot());
        }
        if (this.els.fullscreenBtn) {
            this.els.fullscreenBtn.addEventListener("click", () => this.#toggleFullscreen());
            this.els.fullscreenBtn.addEventListener("touchend", e => {
                e.preventDefault();
                this.#toggleFullscreen();
            });
        }
        if (this.els.zoomInBtn) {
            this.els.zoomInBtn.addEventListener("click", () => this.#zoomIn());
            this.els.zoomInBtn.addEventListener("touchend", e => {
                e.preventDefault();
                this.#zoomIn();
            });
        }
        if (this.els.zoomOutBtn) {
            this.els.zoomOutBtn.addEventListener("click", () => this.#zoomOut());
            this.els.zoomOutBtn.addEventListener("touchend", e => {
                e.preventDefault();
                this.#zoomOut();
            });
        }
        if (this.els.zoomResetBtn) {
            this.els.zoomResetBtn.addEventListener("click", () => this.resetZoom());
            this.els.zoomResetBtn.addEventListener("touchend", e => {
                e.preventDefault();
                this.resetZoom();
            });
        }
        document.addEventListener("scroll", () => {
            this.#hideTooltip();
            this.#hideClickTooltip();
        }, {
            passive: true
        });
        if (this.els.clickTooltipClose) {
            this.els.clickTooltipClose.addEventListener("click", e => {
                e.stopPropagation();
                this.#hideClickTooltip();
            });
        }
        (this.root === document ? document : this.root).addEventListener("click", e => {
            if (this.els.clickTooltip && !this.els.clickTooltip.contains(e.target) && !this.canvas?.contains(e.target)) {
                this.#hideClickTooltip();
            }
        });
        if (this.root !== document) {
            document.addEventListener("click", e => {
                const host = this.root.host;
                if (host && host.contains(e.target)) return;
                this.#hideClickTooltip();
            });
        }
        const themeObserver = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                if (mutation.attributeName === "data-theme") {
                    this.#extractColorsFromCSS();
                    this.#render();
                }
            });
        });
        themeObserver.observe(this.hostEl, {
            attributes: true,
            attributeFilter: [ "data-theme" ]
        });
        this.#initSettingsDropdown();
    }
    #initSettingsDropdown() {
        if (!this.els.settings) return;
        if (this._initCustomSelects) {
            this._initCustomSelects(`#${this.CONFIG.ID_PREFIX}heatmapSettings`);
        }
        const checkboxOptions = this.els.settings.querySelectorAll(".checkbox-options .select-option");
        checkboxOptions.forEach(opt => {
            opt.addEventListener("click", e => {
                e.stopPropagation();
                opt.classList.toggle("selected");
                if (opt.dataset.value === "show-logo") {
                    this.CONFIG.SHOW_LOGO = opt.classList.contains("selected");
                    this.#savePreferences();
                    this.#render();
                }
                if (opt.dataset.value === "border-radius") {
                    const radiusValue = parseInt(opt.dataset.radius) || 6;
                    this.CONFIG.BOX_BORDER_RADIUS = opt.classList.contains("selected") ? radiusValue : 0;
                    this.#savePreferences();
                    this.#render();
                }
            });
        });
        const radioGroups = this.els.settings.querySelectorAll(".radio-options");
        radioGroups.forEach(group => {
            const options = group.querySelectorAll(".select-option");
            options.forEach(opt => {
                opt.addEventListener("click", e => {
                    e.stopPropagation();
                    options.forEach(o => o.classList.remove("selected"));
                    opt.classList.add("selected");
                    const value = opt.dataset.value;
                    if (group.classList.contains("display-name-options")) {
                        if (value === "display-symbol") {
                            this.CONFIG.DISPLAY_NAME = "symbol";
                        } else if (value === "display-name") {
                            this.CONFIG.DISPLAY_NAME = "name";
                        } else {
                            this.CONFIG.DISPLAY_NAME = "none";
                        }
                        this.#savePreferences();
                        this.#render();
                    }
                    if (group.classList.contains("display-value-options")) {
                        if (value === "value-price") this.CONFIG.DISPLAY_VALUE = "price"; else if (value === "value-marketcap") this.CONFIG.DISPLAY_VALUE = "marketcap"; else if (value === "value-none") this.CONFIG.DISPLAY_VALUE = "none"; else this.CONFIG.DISPLAY_VALUE = "selected";
                        this.#savePreferences();
                        this.#render();
                    }
                    if (group.classList.contains("color-mode-options")) {
                        this.CONFIG.COLOR_MODE = value === "color-fixed" ? "fixed" : "gradient";
                        this.#savePreferences();
                        this.#render();
                    }
                });
            });
        });
    }
    #initSelectors() {
        this.#initSelector("heatmapTypeSelect", (value, option) => {
            this.CONFIG.TYPE = value;
            this.state.typeParams = parseParamsFromElement(option);
            this.#savePreferences();
            this.#loadData();
        });
        if (this.els.countrySelect) {
            this.#populateCountryOptions();
            if (typeof initCustomSelects === "function") {
                initCustomSelects(`#${this.CONFIG.ID_PREFIX}heatmapCountrySelect`);
            }
            const countryEventTarget = this.els.countryDropdown || this.els.countrySelect;
            countryEventTarget.addEventListener("click", e => {
                const option = e.target.closest(".select-option");
                if (!option) return;
                const value = option.dataset.value;
                if (!value || value === this.CONFIG.COUNTRY) return;
                this.CONFIG.COUNTRY = value;
                this.CONFIG.INDICES = null;
                this.CONFIG.SECTOR = null;
                this.#indicesLoaded = false;
                this.state.availableSectors = null;
                this.#savePreferences();
                const trigger = this.els.countrySelect.querySelector(".select-trigger");
                if (trigger) {
                    const optionImg = option.querySelector(".select-option-img");
                    const triggerSpan = trigger.querySelector(".select-option-img");
                    if (optionImg && triggerSpan) {
                        triggerSpan.innerHTML = optionImg.innerHTML;
                    }
                }
                countryEventTarget.querySelectorAll(".select-option").forEach(opt => opt.classList.remove("selected"));
                option.classList.add("selected");
                this._scrollToCenter?.(this.els.countrySelect, this.root.querySelector(".data-controls-left"));
                this.#resetIndicesDropdown();
                this.#resetSectorDropdown();
                this.#loadData();
            });
            if (this.els.primaryToggle) {
                this.CONFIG.IS_PRIMARY = this.els.primaryToggle.checked;
                this.els.primaryToggle.addEventListener("change", () => {
                    this.CONFIG.IS_PRIMARY = this.els.primaryToggle.checked;
                    this.#updatePrimaryIconVisibility();
                    this.#savePreferences();
                    this.#loadData();
                });
            }
        }
        if (this.els.indicesSelect) {
            this.#indicesLoaded = false;
            if (typeof initCustomSelects === "function") {
                initCustomSelects(`#${this.CONFIG.ID_PREFIX}heatmapIndicesSelect`);
            }
            const trigger = this.els.indicesSelect.querySelector(".select-trigger");
            if (trigger) {
                trigger.addEventListener("click", () => {
                    if (!this.#indicesLoaded) {
                        this.#loadIndicesList();
                    }
                });
            }
            if (this.CONFIG.INDICES) {
                this.#loadIndicesList();
            }
            const indicesEventTarget = this.els.indicesDropdown || this.els.indicesSelect;
            indicesEventTarget.addEventListener("click", e => {
                const option = e.target.closest(".select-option");
                if (!option) return;
                const value = option.dataset.value;
                const newIndices = value === "all" ? null : value;
                if (newIndices === this.CONFIG.INDICES) return;
                this.CONFIG.INDICES = newIndices;
                const indexNameEl = option.querySelector(".index-name");
                this.state.indicesName = indexNameEl ? indexNameEl.textContent : null;
                const indexLogoEl = option.querySelector(".flag-img");
                this.state.indicesLogo = indexLogoEl ? indexLogoEl.getAttribute("data-src") || indexLogoEl.src : null;
                this.#savePreferences();
                const indicesTrigger = this.els.indicesSelect.querySelector(".select-trigger");
                if (indicesTrigger) {
                    const optionImg = option.querySelector(".select-option-img");
                    const triggerSpan = indicesTrigger.querySelector(".select-option-img");
                    if (optionImg && triggerSpan) {
                        triggerSpan.innerHTML = optionImg.innerHTML;
                    }
                }
                indicesEventTarget.querySelectorAll(".select-option").forEach(opt => opt.classList.remove("selected"));
                option.classList.add("selected");
                this._scrollToCenter?.(this.els.indicesSelect, this.root.querySelector(".data-controls-left"));
                this.#loadData();
            });
        }
        if (this.els.sectorSelect) {
            this.#populateSectorOptions();
            if (typeof initCustomSelects === "function") {
                initCustomSelects(`#${this.CONFIG.ID_PREFIX}heatmapSectorSelect`);
            }
            const sectorEventTarget = this.els.sectorDropdown || this.els.sectorSelect;
            sectorEventTarget.addEventListener("click", e => {
                const option = e.target.closest(".select-option");
                if (!option) return;
                const value = option.dataset.value;
                if (value === this.CONFIG.SECTOR) return;
                this.CONFIG.SECTOR = value === "all" ? null : value;
                this.#savePreferences();
                const triggerText = this.els.sectorSelect.querySelector(".select-trigger-text");
                if (triggerText) {
                    triggerText.textContent = option.textContent.trim();
                }
                sectorEventTarget.querySelectorAll(".select-option").forEach(opt => opt.classList.remove("selected"));
                option.classList.add("selected");
                this._scrollToCenter?.(this.els.sectorSelect, this.root.querySelector(".data-controls-left"));
                this.#loadData();
            });
        }
        if (this.els.sortSelect) {
            if (typeof initCustomSelects === "function") {
                initCustomSelects(`#${this.CONFIG.ID_PREFIX}heatmapSortSelect`);
            }
            const sortEventTarget = this.els.sortDropdown || this.els.sortSelect;
            sortEventTarget.addEventListener("click", e => {
                const option = e.target.closest(".select-option");
                if (!option) return;
                const value = option.dataset.value;
                if (!value || value === this.CONFIG.SORT_VALUE) return;
                this.CONFIG.SORT_VALUE = value;
                this.state.sortParams = parseParamsFromElement(option);
                if (value === "mono") {
                    const firstNonMono = sortEventTarget.querySelector('.select-option:not([data-value="mono"])');
                    this.CONFIG.SIZE_LABEL = firstNonMono ? firstNonMono.textContent.trim() : option.textContent.trim();
                } else {
                    this.CONFIG.SIZE_LABEL = option.textContent.trim();
                }
                this.#savePreferences();
                const triggerText = this.els.sortSelect.querySelector(".select-trigger-text");
                if (triggerText) {
                    triggerText.textContent = option.textContent.trim();
                }
                sortEventTarget.querySelectorAll(".select-option").forEach(opt => opt.classList.remove("selected"));
                option.classList.add("selected");
                this._scrollToCenter?.(this.els.sortSelect, this.root.querySelector(".data-controls-left"));
                this.#updateSettingsDOMFromState();
                this.#loadData();
            });
        }
        if (this.els.colorSelect) {
            if (typeof initCustomSelects === "function") {
                initCustomSelects(`#${this.CONFIG.ID_PREFIX}heatmapColorSelect`);
            }
            const colorEventTarget = this.els.colorDropdown || this.els.colorSelect;
            colorEventTarget.addEventListener("click", e => {
                const option = e.target.closest(".select-option");
                if (!option) return;
                const newParams = parseParamsFromElement(option);
                if (newParams.columns === this.state.colorParams?.columns && newParams.period === this.state.colorParams?.period) return;
                this.state.colorParams = newParams;
                this.CONFIG.COLOR_LABEL = option.textContent.trim();
                const rangeStr = option.dataset.range || "-38,-25.3,-12.7,0,12.7,25.3,38";
                this.CONFIG.COLOR_THRESHOLDS = rangeStr.split(",").map(v => parseFloat(v.trim()));
                this.#savePreferences();
                const triggerText = this.els.colorSelect.querySelector(".select-trigger-text");
                if (triggerText) {
                    triggerText.textContent = option.textContent.trim();
                }
                colorEventTarget.querySelectorAll(".select-option").forEach(opt => opt.classList.remove("selected"));
                option.classList.add("selected");
                this._scrollToCenter?.(this.els.colorSelect, this.root.querySelector(".data-controls-left"));
                this.#updateSettingsDOMFromState();
                this.#loadData();
            });
        }
        this.#initSelector("heatmapPerPageSelect", value => {
            this.CONFIG.PER_PAGE = parseInt(value);
            this.#savePreferences();
            this.#loadData();
        });
    }
    #initSelector(id, onChange) {
        const selectMap = {
            "heatmapTypeSelect": {
                select: this.els.typeSelect,
                dropdown: this.els.typeDropdown
            },
            "heatmapPerPageSelect": {
                select: this.els.perPageSelect,
                dropdown: this.els.perPageSelect
            }
        };
        const cached = selectMap[id];
        if (!cached?.select) return;
        const {select: select, dropdown: dropdown} = cached;
        if (this._initCustomSelects) {
            this._initCustomSelects(`#${this.CONFIG.ID_PREFIX}${id}`);
        }
        const eventTarget = dropdown || select;
        eventTarget.addEventListener("click", e => {
            const option = e.target.closest(".select-option");
            if (!option) return;
            const value = option.dataset.value;
            if (!value) return;
            const triggerText = select.querySelector(".select-trigger-text");
            if (triggerText) {
                triggerText.textContent = option.textContent.trim();
            }
            eventTarget.querySelectorAll(".select-option").forEach(opt => opt.classList.remove("selected"));
            option.classList.add("selected");
            this._scrollToCenter?.(select, this.root.querySelector(".data-controls-left"));
            onChange(value, option);
        });
    }
    #zoomIn() {
        if (this.state.zoomLevel < this.CONFIG.MAX_ZOOM) {
            const oldZoom = this.state.zoomLevel;
            this.state.zoomLevel = Math.min(this.CONFIG.MAX_ZOOM, this.state.zoomLevel + this.CONFIG.ZOOM_STEP);
            const centerX = this.baseWidth / 2;
            const centerY = this.baseHeight / 2;
            const zoomFactor = this.state.zoomLevel / oldZoom;
            this.state.panX = centerX - (centerX - this.state.panX) * zoomFactor;
            this.state.panY = centerY - (centerY - this.state.panY) * zoomFactor;
            this.#constrainPan();
            this.#render();
            this.#updateCursor();
        }
    }
    #zoomOut() {
        if (this.state.zoomLevel > this.CONFIG.MIN_ZOOM) {
            const oldZoom = this.state.zoomLevel;
            this.state.zoomLevel = Math.max(this.CONFIG.MIN_ZOOM, this.state.zoomLevel - this.CONFIG.ZOOM_STEP);
            const centerX = this.baseWidth / 2;
            const centerY = this.baseHeight / 2;
            const zoomFactor = this.state.zoomLevel / oldZoom;
            this.state.panX = centerX - (centerX - this.state.panX) * zoomFactor;
            this.state.panY = centerY - (centerY - this.state.panY) * zoomFactor;
            this.#constrainPan();
            this.#render();
            this.#updateCursor();
        }
    }
    #constrainPan() {
        const scaledWidth = this.baseWidth * this.state.zoomLevel;
        const scaledHeight = this.baseHeight * this.state.zoomLevel;
        const maxPanX = Math.max(0, (scaledWidth - this.baseWidth) / 2);
        const maxPanY = Math.max(0, (scaledHeight - this.baseHeight) / 2);
        this.state.panX = Math.max(-maxPanX, Math.min(maxPanX, this.state.panX));
        this.state.panY = Math.max(-maxPanY, Math.min(maxPanY, this.state.panY));
        if (this.state.zoomLevel === 1) {
            this.state.panX = 0;
            this.state.panY = 0;
        }
    }
    #updateCursor() {
        if (!this.canvas) return;
        this.canvas.style.cursor = this.state.zoomLevel > 1 ? "grab" : "default";
        this.#updateResetButtonVisibility();
    }
    #updateResetButtonVisibility() {
        if (!this.els.zoomResetBtn) return;
        if (this.state.zoomLevel > 1) {
            this.els.zoomResetBtn.classList.add("visible");
        } else {
            this.els.zoomResetBtn.classList.remove("visible");
        }
    }
    #buildRequestData() {
        const baseColumns = [ "perf.rank", "perf.market_cap", "perf.24h_vol_cmc", "active.c", "active.ch", "active.chp", "profile", "ticker", "earnings.market_cap" ];
        const requestData = {
            per_page: this.CONFIG.PER_PAGE
        };
        if (this.state.typeParams) {
            for (const [key, value] of Object.entries(this.state.typeParams)) {
                if (key === "columns") {
                    if (!baseColumns.includes(value)) {
                        baseColumns.push(value);
                    }
                } else if (key === "filters") {
                    requestData.filters = typeof value === "object" ? JSON.stringify(value) : value;
                } else {
                    requestData[key] = value;
                }
            }
        }
        if (this.state.sortParams) {
            for (const [key, value] of Object.entries(this.state.sortParams)) {
                if (key === "columns") {
                    if (!baseColumns.includes(value)) {
                        baseColumns.push(value);
                    }
                } else {
                    requestData[key] = value;
                }
            }
        }
        if (this.state.colorParams) {
            for (const [key, value] of Object.entries(this.state.colorParams)) {
                if (key === "columns") {
                    if (!baseColumns.includes(value)) {
                        baseColumns.push(value);
                    }
                } else {
                    requestData[key] = value;
                }
            }
        }
        requestData.columns = baseColumns.join(",");
        const typeParam = this.state.typeParams?.type || "coin";
        if (typeParam === "stock") {
            if (this.CONFIG.COUNTRY) {
                requestData.country = this.CONFIG.COUNTRY;
            }
            if (this.CONFIG.INDICES) {
                requestData.indices = this.CONFIG.INDICES;
            }
            if (this.CONFIG.SECTOR) {
                requestData.sector = this.CONFIG.SECTOR;
            }
            if (this.CONFIG.IS_PRIMARY) {
                requestData.is_primary = 1;
            }
        }
        return requestData;
    }
    async #loadData(silent = false) {
        if (this.state.isLoading) return;
        this.state.isLoading = true;
        if (!silent) {
            this.#showLoading(true);
        }
        try {
            const requestData = this.#buildRequestData();
            const typeParam = this.state.typeParams.type || "coin";
            const apiEndpoint = typeParam === "stock" ? `${this._apiEndpoint}stock/advance` : `${this._apiEndpoint}crypto/advance`;
            const response = await this._sendPostRequest(apiEndpoint, requestData);
            if (response?.status && response.response && response.response.length > 0) {
                this.state.data = this.#processData(response.response);
                if (!this.state.availableSectors && response.info?.available_sector) {
                    const sectorsStr = response.info.available_sector;
                    if (sectorsStr && sectorsStr.trim() !== "") {
                        this.state.availableSectors = sectorsStr.split(",").map(s => s.trim());
                        if (this.els.sectorSelect) {
                            this.#populateSectorOptions();
                        }
                        this.#savePreferences();
                    }
                }
                if (this.state.data.length === 0) {
                    this.#showEmptyState("No data available for the selected filters");
                    return;
                }
                this.#hideEmptyState();
                if (!silent) {
                    this.state.zoomLevel = 1;
                    this.state.panX = 0;
                    this.state.panY = 0;
                }
                this.#calculateLayout();
                this.#render();
                this.#preloadImages().then(() => this.#render());
            } else {
                this.state.data = [];
                this.#showEmptyState("No data available for the selected criteria");
            }
        } catch (error) {
            console.error("Heatmap data load error:", error);
            this.state.data = [];
            this.#showEmptyState("Unable to load data. Please try again later.");
        } finally {
            this.state.isLoading = false;
            if (!silent) {
                this.#showLoading(false);
            }
            this.#startAutoUpdate();
        }
    }
    #startAutoUpdate() {
        this.#stopAutoUpdate();
        if (this.CONFIG.AUTO_UPDATE && this.CONFIG.AUTO_UPDATE_INTERVAL > 0) {
            this.state.autoUpdateTimer = setInterval(() => {
                this.#loadData(true);
            }, this.CONFIG.AUTO_UPDATE_INTERVAL);
        }
    }
    #stopAutoUpdate() {
        if (this.state.autoUpdateTimer) {
            clearInterval(this.state.autoUpdateTimer);
            this.state.autoUpdateTimer = null;
        }
    }
    #processData(rawData) {
        const processed = [];
        const typeParam = this.state.typeParams?.type || "coin";
        const isStockType = typeParam === "stock";
        const sortField = this.state.sortParams?.columns || "perf.market_cap";
        const colorField = this.state.colorParams?.columns || "active.chp";
        let minPerf = 0;
        let maxPerf = 0;
        for (let i = 0; i < rawData.length; i++) {
            const item = rawData[i];
            if (!item.profile || !item.active) continue;
            if (!isStockType && !item.perf) continue;
            const profile = item.profile;
            const active = item.active;
            const perf = item.perf || {};
            const earnings = item.earnings || {};
            let sizeValue = 0;
            let tooltipSizeValue = 0;
            if (sortField.startsWith("perf.")) {
                const perfKey = sortField.replace("perf.", "");
                tooltipSizeValue = parseFloat(perf[perfKey]) || 0;
            } else if (sortField.startsWith("active.")) {
                const activeKey = sortField.replace("active.", "");
                tooltipSizeValue = parseFloat(active[activeKey]) || 0;
            } else if (sortField.startsWith("earnings.")) {
                const earningsKey = sortField.replace("earnings.", "");
                tooltipSizeValue = parseFloat(earnings[earningsKey]) || 0;
            } else {
                tooltipSizeValue = parseFloat(perf[sortField]) || parseFloat(active[sortField]) || parseFloat(earnings[sortField]) || 0;
            }
            if (this.CONFIG.SORT_VALUE === "mono") {
                sizeValue = 1;
            } else {
                sizeValue = tooltipSizeValue;
            }
            if (isStockType && sizeValue <= 0) {
                sizeValue = 1;
            }
            if (this.CONFIG.SORT_VALUE !== "mono" && !isStockType && sizeValue <= 0) continue;
            let performanceValue = 0;
            if (colorField === "active.chp") {
                performanceValue = parseFloat(active.chp) || 0;
            } else if (colorField.startsWith("perf.")) {
                const perfKey = colorField.replace("perf.", "");
                performanceValue = parseFloat(perf[perfKey]) || 0;
            } else if (colorField.startsWith("active.")) {
                const activeKey = colorField.replace("active.", "");
                performanceValue = parseFloat(active[activeKey]) || 0;
            }
            if (performanceValue < minPerf) minPerf = performanceValue;
            if (performanceValue > maxPerf) maxPerf = performanceValue;
            processed.push({
                id: item.ticker || profile.symbol,
                ticker: item.ticker || profile.symbol,
                symbol: profile.symbol || item.ticker,
                name: profile.name || item.ticker,
                logo: profile.self_logo || profile.base_logo || "",
                currency: profile.currency || "USD",
                exchange: profile.exchange || "",
                price: parseFloat(active.c) || 0,
                change: parseFloat(active.ch) || 0,
                changePercent: parseFloat(active.chp) || 0,
                performanceValue: performanceValue,
                marketCap: parseFloat(earnings?.market_cap) || parseFloat(item.earnings?.market_cap) || parseFloat(perf.market_cap) || 0,
                volume: parseFloat(perf["24h_vol_cmc"]) || parseFloat(active.v) || 0,
                sizeValue: sizeValue,
                tooltipSizeValue: tooltipSizeValue,
                rank: parseInt(perf.rank) || 0
            });
        }
        processed.sort((a, b) => b.sizeValue - a.sizeValue);
        this.#updateDynamicThresholds();
        return processed;
    }
    #updateDynamicThresholds() {
        this.state.minPerformance = this.CONFIG.COLOR_THRESHOLDS[0];
        this.state.maxPerformance = this.CONFIG.COLOR_THRESHOLDS[6];
        this.#updateLegendLabels();
    }
    #updateLegendLabels() {
        if (!this.els.legendBoxes) return;
        const thresholds = this.CONFIG.COLOR_THRESHOLDS;
        const labels = this.els.legendBoxes.querySelectorAll(".legend-box-label");
        if (labels.length === 7) {
            labels[0].textContent = `${thresholds[0]}%`;
            labels[1].textContent = `${thresholds[1]}%`;
            labels[2].textContent = `${thresholds[2]}%`;
            labels[3].textContent = "0%";
            labels[4].textContent = `${thresholds[4]}%`;
            labels[5].textContent = `${thresholds[5]}%`;
            labels[6].textContent = `${thresholds[6]}%`;
            labels.forEach(label => label.classList.remove("skeleton"));
        }
    }
    async #preloadImages() {
        const promises = this.state.data.map(item => new Promise(resolve => {
            if (!item.logo || this.imageCache.has(item.logo)) {
                resolve();
                return;
            }
            const img = new Image;
            const logoUrl = `${this.iconPath}${item.logo}${this.iconType}`;
            img.onload = () => {
                try {
                    const testCanvas = document.createElement("canvas");
                    testCanvas.width = 1;
                    testCanvas.height = 1;
                    const testCtx = testCanvas.getContext("2d", {
                        willReadFrequently: true
                    });
                    testCtx.drawImage(img, 0, 0);
                    this.imageCache.set(item.logo, img);
                } catch (e) {}
                resolve();
            };
            img.onerror = () => {
                resolve();
            };
            img.src = logoUrl;
        }));
        await Promise.all(promises);
    }
    #calculateLayout() {
        if (this.state.data.length === 0) return;
        const width = this.baseWidth;
        const height = this.baseHeight;
        if (this.CONFIG.SORT_VALUE === "mono") {
            this.state.layout = this.#gridLayout(this.state.data, {
                x: 0,
                y: 0,
                width: width,
                height: height
            });
            return;
        }
        const totalValue = this.state.data.reduce((sum, d) => sum + d.sizeValue, 0);
        const normalizedData = this.state.data.map(d => ({
            ...d,
            normalizedValue: d.sizeValue / totalValue * (width * height)
        }));
        this.state.layout = this.#squarify(normalizedData, {
            x: 0,
            y: 0,
            width: width,
            height: height
        });
    }
    #gridLayout(data, rect) {
        const result = [];
        const count = data.length;
        if (count === 0) return result;
        const {x: rectX, y: rectY, width: width, height: height} = rect;
        const aspectRatio = width / height;
        let cols = Math.ceil(Math.sqrt(count * aspectRatio));
        let rows = Math.ceil(count / cols);
        while (cols * (rows - 1) >= count && rows > 1) {
            rows--;
        }
        const cellWidth = width / cols;
        const cellHeight = height / rows;
        data.forEach((item, index) => {
            const col = index % cols;
            const row = Math.floor(index / cols);
            result.push({
                x: rectX + col * cellWidth,
                y: rectY + row * cellHeight,
                width: cellWidth,
                height: cellHeight,
                data: item
            });
        });
        return result;
    }
    #squarify(data, rect) {
        const result = [];
        if (data.length === 0) return result;
        const items = data.map(d => ({
            ...d
        }));
        const totalValue = items.reduce((sum, d) => sum + d.normalizedValue, 0);
        this.#squarifyRecursive(items, rect, result, totalValue);
        return result;
    }
    #squarifyRecursive(items, rect, result, totalValue, isFirstCall = true) {
        if (items.length === 0) return;
        if (items.length === 1) {
            result.push({
                x: rect.x,
                y: rect.y,
                width: rect.width,
                height: rect.height,
                data: items[0]
            });
            return;
        }
        const isNarrowScreen = this.baseWidth < 600;
        const forceVertical = isNarrowScreen && isFirstCall;
        const isHorizontal = forceVertical ? false : rect.width >= rect.height;
        const side = isHorizontal ? rect.height : rect.width;
        let rowItems = [];
        let rowArea = 0;
        let worstRatio = Infinity;
        for (let i = 0; i < items.length; i++) {
            const item = items[i];
            const newRowItems = [ ...rowItems, item ];
            const newRowArea = rowArea + item.normalizedValue;
            const newRatio = this.#worst(newRowItems, newRowArea, side);
            if (newRatio <= worstRatio) {
                rowItems = newRowItems;
                rowArea = newRowArea;
                worstRatio = newRatio;
            } else {
                break;
            }
        }
        const rowSize = rowArea / side;
        if (isHorizontal) {
            let y = rect.y;
            for (let i = 0; i < rowItems.length; i++) {
                const item = rowItems[i];
                const height = item.normalizedValue / rowSize;
                result.push({
                    x: rect.x,
                    y: y,
                    width: rowSize,
                    height: height,
                    data: item
                });
                y += height;
            }
            const remainingItems = items.slice(rowItems.length);
            const remainingRect = {
                x: rect.x + rowSize,
                y: rect.y,
                width: rect.width - rowSize,
                height: rect.height
            };
            this.#squarifyRecursive(remainingItems, remainingRect, result, totalValue, false);
        } else {
            let x = rect.x;
            for (let i = 0; i < rowItems.length; i++) {
                const item = rowItems[i];
                const width = item.normalizedValue / rowSize;
                result.push({
                    x: x,
                    y: rect.y,
                    width: width,
                    height: rowSize,
                    data: item
                });
                x += width;
            }
            const remainingItems = items.slice(rowItems.length);
            const remainingRect = {
                x: rect.x,
                y: rect.y + rowSize,
                width: rect.width,
                height: rect.height - rowSize
            };
            this.#squarifyRecursive(remainingItems, remainingRect, result, totalValue, false);
        }
    }
    #worst(items, totalArea, side) {
        if (items.length === 0) return Infinity;
        const rowSize = totalArea / side;
        let maxRatio = 0;
        for (let i = 0; i < items.length; i++) {
            const itemSize = items[i].normalizedValue / rowSize;
            const ratio = Math.max(rowSize / itemSize, itemSize / rowSize);
            maxRatio = Math.max(maxRatio, ratio);
        }
        return maxRatio;
    }
    #render() {
        if (!this.ctx || this.state.layout.length === 0) return;
        const ctx = this.ctx;
        const zoom = this.state.zoomLevel;
        const panX = this.state.panX;
        const panY = this.state.panY;
        ctx.clearRect(0, 0, this.baseWidth, this.baseHeight);
        ctx.save();
        ctx.translate(this.baseWidth / 2 + panX, this.baseHeight / 2 + panY);
        ctx.scale(zoom, zoom);
        ctx.translate(-this.baseWidth / 2, -this.baseHeight / 2);
        for (let i = 0; i < this.state.layout.length; i++) {
            const item = this.state.layout[i];
            this.#drawItem(ctx, item, zoom);
        }
        ctx.restore();
    }
    #drawItem(ctx, item, zoom = 1, skipLogo = false) {
        const {x: x, y: y, width: width, height: height, data: data} = item;
        const inverseZoom = 1 / zoom;
        const gap = this.CONFIG.BOX_GAP / 2 * inverseZoom;
        const color = this.#getColor(data.performanceValue);
        ctx.direction = "ltr";
        const boxX = x + gap, boxY = y + gap;
        const boxW = width - gap * 2, boxH = height - gap * 2;
        const borderRadius = this.CONFIG.BOX_BORDER_RADIUS * inverseZoom;
        ctx.fillStyle = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
        if (borderRadius > 0) {
            this.#drawRoundedRect(ctx, boxX, boxY, boxW, boxH, borderRadius);
            ctx.fill();
        } else {
            ctx.fillRect(boxX, boxY, boxW, boxH);
        }
        if (!skipLogo && this.state.hoveredItem?.symbol === data.symbol) {
            const hoverColor = this.CONFIG.HOVER_BORDER_COLOR || getComputedStyle(this.hostEl).getPropertyValue("--primary-blue").trim() || "#2962ff";
            ctx.strokeStyle = hoverColor;
            ctx.lineWidth = this.CONFIG.HOVER_BORDER_WIDTH * inverseZoom;
            borderRadius > 0 ? (this.#drawRoundedRect(ctx, boxX, boxY, boxW, boxH, borderRadius), 
            ctx.stroke()) : ctx.strokeRect(boxX, boxY, boxW, boxH);
        }
        const effectiveW = width * zoom, effectiveH = height * zoom;
        if (effectiveW < this.CONFIG.MIN_BOX_SIZE || effectiveH < this.CONFIG.MIN_BOX_SIZE) return;
        const {FONT_FAMILY: fontFamily, TEXT_FILL_COLOR: textFillColor, TEXT_SHADOW_COLOR: shadowColor, TEXT_SHADOW_BLUR: shadowBlurMult} = this.CONFIG;
        const isMonoMode = this.CONFIG.SORT_VALUE === "mono";
        const smallerDim = Math.min(effectiveW, effectiveH);
        const baseFontSize = isMonoMode ? this.CONFIG.MONO_FONT_SIZE : this.CONFIG.SYMBOL_FONT_SIZE;
        const fontSize = Math.max(this.CONFIG.MIN_FONT_SIZE, Math.min(baseFontSize, smallerDim * this.CONFIG.FONT_SIZE_SCALE_FACTOR));
        const displayText = this.CONFIG.DISPLAY_NAME === "symbol" ? data.symbol : this.CONFIG.DISPLAY_NAME === "name" ? data.name : null;
        const logoSizeBase = Math.min(this.CONFIG.LOGO_SIZE_MAX, Math.max(this.CONFIG.LOGO_SIZE_MIN, smallerDim * this.CONFIG.LOGO_SIZE_RATIO));
        const showLogo = !skipLogo && this.CONFIG.SHOW_LOGO && effectiveW >= logoSizeBase + 10 && effectiveH >= logoSizeBase + 10;
        let runningH = showLogo ? logoSizeBase + 6 : 0;
        const showValue = this.CONFIG.DISPLAY_VALUE !== "none" && effectiveW >= 50 && effectiveH >= runningH + fontSize + 12;
        if (showValue) runningH += fontSize + 6;
        const showSymbol = displayText && effectiveW > 45 && effectiveH >= runningH + fontSize + 8;
        const centerX = x + width / 2, centerY = y + height / 2;
        const actualLogoSize = logoSizeBase * inverseZoom;
        const actualFontSize = fontSize * inverseZoom;
        const contentGap = 6 * inverseZoom;
        let contentH = 0;
        if (showLogo) contentH += actualLogoSize + (showValue || showSymbol ? contentGap : 0);
        if (showValue) contentH += actualFontSize + (showSymbol ? contentGap : 0);
        if (showSymbol) contentH += actualFontSize;
        let currentY = centerY - contentH / 2;
        if (showLogo) {
            if (data.logo) {
                const img = this.imageCache.get(data.logo);
                if (img) {
                    try {
                        ctx.save();
                        ctx.beginPath();
                        ctx.arc(centerX, currentY + actualLogoSize / 2, actualLogoSize / 2, 0, Math.PI * 2);
                        ctx.clip();
                        ctx.drawImage(img, centerX - actualLogoSize / 2, currentY, actualLogoSize, actualLogoSize);
                        ctx.restore();
                    } catch (e) {}
                }
            }
            currentY += actualLogoSize + (showValue || showSymbol ? contentGap : 0);
        }
        if (showValue) {
            const perfSign = data.performanceValue > 0 ? "+" : data.performanceValue < 0 ? "−" : "";
            const valueText = this.CONFIG.DISPLAY_VALUE === "price" ? this._addThousandSeparator(data.price) : this.CONFIG.DISPLAY_VALUE === "marketcap" ? convertNumberToShortForm(data.tooltipSizeValue) : `${perfSign}${Math.abs(data.performanceValue).toFixed(2)}%`;
            ctx.fillStyle = textFillColor;
            ctx.font = `700 ${actualFontSize}px ${fontFamily}`;
            ctx.textAlign = "center";
            ctx.textBaseline = "middle";
            ctx.shadowColor = shadowColor;
            ctx.shadowBlur = shadowBlurMult * inverseZoom;
            ctx.fillText(valueText, centerX, currentY + actualFontSize / 2);
            ctx.shadowBlur = 0;
            currentY += actualFontSize + (showSymbol ? contentGap : 0);
        }
        if (showSymbol) {
            ctx.fillStyle = textFillColor;
            ctx.font = `700 ${actualFontSize}px ${fontFamily}`;
            ctx.textAlign = "center";
            ctx.textBaseline = "middle";
            ctx.shadowColor = shadowColor;
            ctx.shadowBlur = shadowBlurMult * inverseZoom;
            const maxW = width - 10 * inverseZoom;
            let text = displayText;
            if (ctx.measureText(text).width > maxW) {
                while (text.length > 0 && ctx.measureText(text + "...").width > maxW) text = text.slice(0, -1);
                text = text.length > 0 ? text + "..." : displayText.charAt(0);
            }
            ctx.fillText(text, centerX, currentY + actualFontSize / 2);
            ctx.shadowBlur = 0;
        }
    }
    #drawRoundedRect(ctx, x, y, width, height, radius) {
        const r = Math.min(radius, width / 2, height / 2);
        ctx.beginPath();
        ctx.moveTo(x + r, y);
        ctx.lineTo(x + width - r, y);
        ctx.quadraticCurveTo(x + width, y, x + width, y + r);
        ctx.lineTo(x + width, y + height - r);
        ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
        ctx.lineTo(x + r, y + height);
        ctx.quadraticCurveTo(x, y + height, x, y + height - r);
        ctx.lineTo(x, y + r);
        ctx.quadraticCurveTo(x, y, x + r, y);
        ctx.closePath();
    }
    #getItemAtPosition(mouseX, mouseY) {
        const zoom = this.state.zoomLevel;
        const panX = this.state.panX;
        const panY = this.state.panY;
        const canvasX = (mouseX - this.baseWidth / 2 - panX) / zoom + this.baseWidth / 2;
        const canvasY = (mouseY - this.baseHeight / 2 - panY) / zoom + this.baseHeight / 2;
        for (let i = 0; i < this.state.layout.length; i++) {
            const item = this.state.layout[i];
            if (canvasX >= item.x && canvasX <= item.x + item.width && canvasY >= item.y && canvasY <= item.y + item.height) {
                return item;
            }
        }
        return null;
    }
    #getColor(val) {
        const t = this.CONFIG.COLOR_THRESHOLDS;
        const c = this.CONFIG.colors;
        if (this.CONFIG.COLOR_MODE === "fixed") {
            const neutralT = Math.abs(t[4]) * .1;
            if (val <= t[0]) return c.negative[0];
            if (val <= t[1]) return c.negative[1];
            if (val < -neutralT) return c.negative[2];
            if (val <= neutralT) return c.neutral;
            if (val <= t[4]) return c.positive[0];
            if (val <= t[5]) return c.positive[1];
            return c.positive[2];
        }
        if (val <= t[0]) return c.negative[0];
        if (val >= t[6]) return c.positive[2];
        const colorMap = [ [ t[0], t[1], c.negative[0], c.negative[1] ], [ t[1], t[2], c.negative[1], c.negative[2] ], [ t[2], t[3], c.negative[2], c.neutral ], [ t[3], t[4], c.neutral, c.positive[0] ], [ t[4], t[5], c.positive[0], c.positive[1] ], [ t[5], t[6], c.positive[1], c.positive[2] ] ];
        for (const [min, max, c1, c2] of colorMap) {
            if (val < max) {
                const f = (val - min) / (max - min);
                return [ Math.round(c1[0] + (c2[0] - c1[0]) * f), Math.round(c1[1] + (c2[1] - c1[1]) * f), Math.round(c1[2] + (c2[2] - c1[2]) * f) ];
            }
        }
        return c.positive[2];
    }
    #positionTooltip(el, event) {
        if (!el?.classList.contains("visible")) return;
        const tRect = el.getBoundingClientRect(), cRect = this.els.container.getBoundingClientRect();
        const clientX = event.clientX, clientY = event.clientY;
        requestAnimationFrame(() => {
            let x = clientX - cRect.left + 15, y = clientY - cRect.top + 15;
            if (x + tRect.width > cRect.width) x = clientX - cRect.left - tRect.width - 15;
            if (y + tRect.height > cRect.height) y = clientY - cRect.top - tRect.height - 15;
            el.style.left = `${Math.max(0, x)}px`;
            el.style.top = `${Math.max(0, y)}px`;
        });
    }
    #showTooltip(event, data) {
        if (!this.CONFIG.ENABLE_HOVER_TOOLTIP || !this.els.tooltip) return;
        this.state.hoveredItem = data;
        const $ = s => this.els.tooltip.querySelector(s);
        const logo = $(".tooltip-logo"), currency = data.currency || "";
        if (logo) {
            logo.src = data.logo ? `${this.iconPath}${data.logo}${this.iconType}` : "";
            logo.style.display = data.logo ? "" : "none";
        }
        const setText = (sel, txt) => {
            const el = $(sel);
            if (el) el.textContent = txt;
        };
        setText(".tooltip-symbol", data.symbol);
        setText(".tooltip-name", data.name);
        setText(".tooltip-price-label", this.CONFIG.PRICE_LABEL);
        setText(".tooltip-price", this._addThousandSeparator(data.price) + " " + currency);
        setText(".tooltip-size-label", this.CONFIG.SIZE_LABEL);
        setText(".tooltip-size-value", convertNumberToShortForm(data.tooltipSizeValue, currency));
        setText(".tooltip-performance-label", this.CONFIG.COLOR_LABEL);
        const perf = $(".tooltip-performance");
        if (perf) {
            const perfSign = data.performanceValue > 0 ? "+" : data.performanceValue < 0 ? "−" : "";
            perf.innerHTML = `<span class="sign">${perfSign}</span>${Math.abs(data.performanceValue).toFixed(2)}%`;
            perf.className = `tooltip-value tooltip-performance ${data.performanceValue > 0 ? "positive" : data.performanceValue < 0 ? "negative" : ""}`;
        }
        this.els.tooltip.classList.add("visible");
        this.#positionTooltip(this.els.tooltip, event);
        this.#render();
    }
    #hideTooltip() {
        if (!this.els.tooltip) return;
        this.els.tooltip.classList.remove("visible");
        this.state.hoveredItem = null;
        this.#render();
    }
    #showClickTooltip(event, data) {
        if (!this.CONFIG.ENABLE_CLICK_TOOLTIP || !this.els.clickTooltip) return;
        this.state.clickedItem = data;
        this.#hideTooltip();
        const $ = s => this.els.clickTooltip.querySelector(s);
        const logo = $(".click-tooltip-logo");
        if (logo) {
            logo.src = data.logo ? `${this.iconPath}${data.logo}${this.iconType}` : "";
            logo.style.display = data.logo ? "" : "none";
        }
        const setText = (sel, txt) => {
            const el = $(sel);
            if (el) el.textContent = txt;
        };
        setText(".click-tooltip-symbol", data.symbol);
        setText(".click-tooltip-name", data.name);
        const exchange = (data.exchange || "CRYPTO").toUpperCase();
        const overviewLink = $(".overview-link"), chartLink = $(".chart-link");
        const typeForUrl = this.CONFIG.TYPE === "stock" || this.CONFIG.TYPE === "indices" ? "stock" : this.CONFIG.TYPE;
        const urlValues = {
            symbol: data.symbol,
            name: data.name,
            exchange: exchange,
            type: typeForUrl,
            ticker: data.ticker || data.id
        };
        const overviewUrl = buildDynamicUrl(this.CONFIG.OVERVIEW_URL || null, urlValues);
        if (overviewLink) {
            overviewLink.href = overviewUrl;
            if (this.CONFIG.LINK_TARGET === "_blank") {
                overviewLink.target = "_blank";
                overviewLink.rel = "noopener noreferrer";
            }
        }
        if (chartLink) {
            chartLink.href = buildDynamicUrl(this.CONFIG.CHART_URL || null, urlValues);
            chartLink.target = "_blank";
            chartLink.rel = "noopener noreferrer";
        }
        this.els.clickTooltip.classList.add("visible");
        this.#positionTooltip(this.els.clickTooltip, event);
    }
    #hideClickTooltip() {
        if (!this.els.clickTooltip) return;
        this.els.clickTooltip.classList.remove("visible");
        this.state.clickedItem = null;
    }
    #showLoading(show) {
        if (!this.els.container) return;
        if (show) {
            if (!this.state.isFirstLoad) this.els.container.classList.add("reloading");
        } else {
            this.els.container.classList.add("data-loaded");
            this.els.container.classList.remove("reloading");
            this.els.container.removeAttribute("aria-busy");
            if (this.els.skeleton) {
                this.els.skeleton.remove();
                this.els.skeleton = null;
            }
            this.state.isFirstLoad = false;
        }
    }
    #showEmptyState(message) {
        if (!this.els.container) return;
        const msgEl = this.els.emptyState?.querySelector(".empty-state-message");
        if (msgEl) msgEl.textContent = message;
        this.els.container.classList.add("is-empty", "data-loaded");
        this.els.container.classList.remove("reloading");
        if (this.els.legendBoxes) {
            const labels = this.els.legendBoxes.querySelectorAll(".legend-box-label");
            labels.forEach(label => {
                label.textContent = "-";
                label.classList.remove("skeleton");
            });
        }
    }
    #hideEmptyState() {
        this.els.container?.classList.remove("is-empty");
    }
    #takeScreenshot() {
        if (!this.canvas) return;
        try {
            const headerH = 44 * this.dpr;
            const tempCanvas = document.createElement("canvas");
            tempCanvas.width = this.canvas.width;
            tempCanvas.height = this.canvas.height + headerH;
            const ctx = tempCanvas.getContext("2d", {
                willReadFrequently: true
            });
            const getCss = (v, d) => getComputedStyle(this.hostEl).getPropertyValue(v).trim() || d;
            ctx.fillStyle = getCss("--bg-secondary", "#1a1f36");
            ctx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
            ctx.fillStyle = getCss("--bg-primary", "#121212");
            ctx.fillRect(0, 0, tempCanvas.width, headerH);
            const isDark = this.hostEl.getAttribute("data-theme") === "dark" || window.matchMedia("(prefers-color-scheme: dark)").matches;
            const font = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
            ctx.fillStyle = isDark ? "#fff" : "#0F172A";
            ctx.font = `bold ${16 * this.dpr}px ${font}`;
            ctx.textAlign = "left";
            ctx.textBaseline = "middle";
            ctx.fillText("Crypto Heatmap", 16 * this.dpr, headerH / 2);
            ctx.fillStyle = isDark ? "rgba(255,255,255,0.7)" : "rgba(15,23,42,0.7)";
            ctx.font = `${14 * this.dpr}px ${font}`;
            ctx.textAlign = "right";
            ctx.fillText((new Date).toLocaleString("en-US", {
                year: "numeric",
                month: "short",
                day: "numeric",
                hour: "2-digit",
                minute: "2-digit",
                hour12: true
            }), tempCanvas.width - 16 * this.dpr, headerH / 2);
            ctx.setTransform(1, 0, 0, 1, 0, 0);
            ctx.translate(0, headerH);
            ctx.scale(this.dpr, this.dpr);
            ctx.translate(this.baseWidth / 2 + this.state.panX, this.baseHeight / 2 + this.state.panY);
            ctx.scale(this.state.zoomLevel, this.state.zoomLevel);
            ctx.translate(-this.baseWidth / 2, -this.baseHeight / 2);
            this.state.layout.forEach(item => this.#drawItem(ctx, item, this.state.zoomLevel, true));
            const link = document.createElement("a");
            link.download = `heatmap-${(new Date).toISOString().split("T")[0]}.png`;
            link.href = tempCanvas.toDataURL("image/png");
            link.click();
        } catch (e) {}
    }
    #toggleFullscreen() {
        const target = this.els.fullscreenWrapper || this.els.container;
        if (!target) return;
        const resize = () => setTimeout(() => {
            this.#resizeCanvas();
            this.#calculateLayout();
            this.#render();
        }, 100);
        const fsEl = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement;
        const isCssFs = target.classList.contains("ios-fullscreen");
        if (fsEl || isCssFs) {
            if (isCssFs) {
                target.classList.remove("ios-fullscreen");
                document.body?.classList.remove("has-ios-fullscreen");
                resize();
            } else {
                (document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen || document.msExitFullscreen)?.call(document);
            }
        } else {
            const reqFs = target.requestFullscreen || target.webkitRequestFullscreen || target.mozRequestFullScreen || target.msRequestFullscreen;
            if (reqFs) {
                const result = reqFs.call(target);
                result?.then?.(resize).catch?.(() => {}) || resize();
            } else {
                target.classList.add("ios-fullscreen");
                document.body?.classList.add("has-ios-fullscreen");
                resize();
            }
        }
    }
    refresh() {
        this.#applyVisibilityConfig();
        this.#loadData();
    }
    getData() {
        return this.state.data;
    }
    resetZoom() {
        this.state.zoomLevel = 1;
        this.state.panX = 0;
        this.state.panY = 0;
        this.#render();
        this.#updateCursor();
    }
}

export { Heatmap };

if (typeof window !== "undefined") {
    window.Heatmap = Heatmap;
}