// custom-chart.js - Canvas-based chart with integrated combine mode
// Replaces Chart.js and drag-combine.js

const VoteChart = (function() {
    'use strict';

    // === PRIVATE STATE ===
    let canvas = null;
    let ctx = null;
    let chartData = []; // [{label, total, youtube, twitch, x, y, width, height}]
    let tooltip = null;
    let hoveredBar = null;
    let tooltipLocked = false; // When true, tooltip stays visible for selected bar
    let lockedTooltipX = 0;
    let lockedTooltipY = 0;
    let dpr = 1; // Device pixel ratio
    let initialized = false; // Track initialization to prevent duplicate listeners
    let lastRenderTime = 0; // Throttle renders
    let pendingRender = false; // Track if a render is scheduled

    // Combine mode state
    let combineMode = false;
    let selectedCandidate = null;
    let combinedGroups = new Map(); // displayName -> [originals]
    let categoryToGroup = new Map(); // original -> displayName
    let _lastStatsMap = null; // Local copy of raw stats for pop-out windows

    // Sort mode state
    let sortMode = 'popular'; // 'popular', 'youtube', 'twitch'

    // Animation state
    let animationProgress = 0; // 0 = combined, 1 = split
    let animationTarget = 0;   // Target state
    let animationId = null;    // requestAnimationFrame ID
    const ANIMATION_DURATION = 300; // ms

    // Drag-and-drop state for combining categories
    let isDragging = false;
    let draggedBar = null;
    let dragX = 0;
    let dragY = 0;
    let dropTarget = null;
    let combineCooldown = false; // Prevent accidental pickups after combining
    let _suppressBroadcast = false; // Prevent broadcast loops during incoming sync

    // Click-and-hold state for initiating drag
    let holdTimer = null;
    let holdStartX = 0;
    let holdStartY = 0;
    let holdBar = null;
    let ignoringClick = false; // Flag to ignore click after drag
    const HOLD_THRESHOLD = 150; // ms to hold before drag starts
    const HOLD_MOVE_TOLERANCE = 8; // px movement allowed during hold

    // Fly-back animation state (when drag is cancelled)
    let flyBackActive = false;
    let flyBackStartX = 0;
    let flyBackStartY = 0;
    let flyBackEndX = 0;
    let flyBackEndY = 0;
    let flyBackProgress = 0;
    let flyBackAnimationId = null;
    const FLY_BACK_DURATION = 250; // ms

    // Label hit detection state (updated during render)
    let labelHitRegions = []; // [{bar, x, y, width, height, rotated}]
    let labelsRotated = false;

    // Trash bin state
    let trashBin = null; // DOM element for trash bin
    let trashedCategories = new Set(); // Categories that have been trashed this poll
    let isOverTrash = false; // Track if dragging over trash bin

    // === CONFIGURATION ===
    const DEFAULT_BOTTOM_PADDING = 60;
    const config = {
        padding: { top: 30, right: 20, bottom: DEFAULT_BOTTOM_PADDING, left: 50 },
        barSpacing: 0.2, // 20% of bar width
        colors: {} // Updated dynamically by getThemeColors()
    };

    function getThemeColors() {
        const isDark = document.documentElement.getAttribute('data-bs-theme') === 'dark';
        return {
            bar: 'rgba(75, 192, 192, 0.7)',
            barBorder: 'rgba(75, 192, 192, 1)',
            youtube: 'rgba(255, 0, 0, 0.7)',
            youtubeBorder: 'rgba(255, 0, 0, 1)',
            twitch: 'rgba(147, 96, 255, 0.7)',
            twitchBorder: 'rgba(147, 96, 255, 1)',
            selected: 'rgba(255, 215, 0, 0.3)',
            selectedBorder: 'rgba(255, 215, 0, 1)',
            grid: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
            text: isDark ? '#e0e0e0' : '#333',
            axis: isDark ? '#aaa' : '#666'
        };
    }
    config.colors = getThemeColors();

    // Get current font size from presets
    function getCurrentFontSize() {
        if (typeof window.fontSizePresets !== 'undefined') {
            const current = window.fontSizePresets.getCurrent();
            const presets = window.fontSizePresets.getPresets();
            return presets[current]?.chartLabels || 12;
        }
        return 12; // default
    }

    function getFont(type) {
        const size = getCurrentFontSize();
        switch (type) {
            case 'label': return size + 'px Arial, sans-serif';
            case 'value': return 'bold ' + size + 'px Arial, sans-serif';
            case 'axis': return (size - 1) + 'px Arial, sans-serif';
            default: return size + 'px Arial, sans-serif';
        }
    }

    // === INITIALIZATION ===
    function init(canvasId) {
        // Prevent duplicate initialization - avoids accumulating event listeners
        if (initialized && canvas) {
            return true;
        }

        canvas = document.getElementById(canvasId);
        if (!canvas) {
            console.error('VoteChart: Canvas not found:', canvasId);
            return false;
        }

        ctx = canvas.getContext('2d');
        dpr = window.devicePixelRatio || 1;

        canvas.addEventListener('mousedown', handleMouseDown);
        canvas.addEventListener('mousemove', handleMouseMove);
        canvas.addEventListener('click', handleClick);
        canvas.addEventListener('dblclick', handleDoubleClick);
        canvas.addEventListener('mouseup', handleMouseUp);
        canvas.addEventListener('mouseleave', handleMouseLeave);
        window.addEventListener('resize', handleResize);
        document.addEventListener('keydown', handleKeyDown);

        // Create tooltip element
        createTooltip();

        // Initialize combine mode UI
        initCombineUI();

        // Sync animation state with source tracking state
        if (isSourceTrackingEnabled()) {
            animationProgress = 1;
            animationTarget = 1;
        } else {
            animationProgress = 0;
            animationTarget = 0;
        }

        // Initialize BroadcastChannel for pop-out sync
        try {
            if (!window._chartBroadcast) {
                window._chartBroadcast = new BroadcastChannel('jongvoter-chart');
            }
        } catch (e) { }

        // Initial sizing
        handleResize();

        initialized = true;
        console.log('VoteChart initialized');
        return true;
    }

    // === COMBINE MODE UI ===
    function initCombineUI() {
        // Prevent duplicate initialization
        if (document.getElementById('toggleCombineMode')) return;

        const widgetContainer2 = document.getElementById('widgetContainer2');
        if (!widgetContainer2) return;

        // Get or create the shared toolbar row
        let toolbarRow = document.getElementById('chartToolbar');
        if (!toolbarRow) {
            toolbarRow = document.createElement('div');
            toolbarRow.id = 'chartToolbar';
            toolbarRow.className = 'd-flex flex-wrap justify-content-center align-items-center gap-0 mb-1 py-1 px-2 border rounded bg-body-tertiary';
            widgetContainer2.appendChild(toolbarRow);
        }

        const combineDiv = document.createElement('div');
        combineDiv.className = 'combine-controls d-flex align-items-center';
        combineDiv.innerHTML = `
            <button class="btn btn-sm btn-outline-primary" id="toggleCombineMode" title="Grab category">
                🫳
            </button>
            <button class="btn btn-sm btn-outline-danger d-none" id="clearCombinations" title="Restore trashed & clear combinations">
                <i class="fas fa-undo"></i>
            </button>
            <span id="combineSelected" class="badge bg-primary d-none"></span>
        `;

        toolbarRow.appendChild(combineDiv);

        // Add source split button (icon button that glows when active)
        const sourceDiv = document.createElement('div');
        sourceDiv.className = 'source-controls d-flex align-items-center ms-1';
        sourceDiv.innerHTML = `
            <button class="btn btn-sm btn-outline-secondary source-split-btn" id="sourceSplitBtn" title="Split by YouTube/Twitch">
                <i class="fab fa-youtube"></i><i class="fab fa-twitch"></i>
            </button>
        `;
        toolbarRow.appendChild(sourceDiv);

        // Add sort dropdown
        const sortDiv = document.createElement('div');
        sortDiv.className = 'sort-controls d-flex align-items-center ms-1';
        sortDiv.innerHTML = `
            <select class="form-select form-select-sm" id="chartSortDropdown" style="width: auto; font-size: 0.7rem; padding: 0.15rem 1.5rem 0.15rem 0.4rem;" title="Sort chart">
                <option value="popular">Most Votes</option>
                <option value="youtube">Most YT</option>
                <option value="twitch">Most TW</option>
            </select>
        `;
        toolbarRow.appendChild(sortDiv);

        // Create trash bin element (positioned relative to chart container)
        createTrashBin();

        // Set up button listeners
        document.getElementById('toggleCombineMode').addEventListener('click', toggleCombineMode);
        document.getElementById('clearCombinations').addEventListener('click', clearCombinations);
        document.getElementById('sourceSplitBtn').addEventListener('click', handleSourceSplitClick);
        document.getElementById('chartSortDropdown').addEventListener('change', handleSortChange);

        // Load saved sort setting
        loadSortSetting();
    }

    function createTrashBin() {
        if (trashBin) return;

        const chartContainer = document.getElementById('chart-js-container');
        if (!chartContainer) return;

        // Make container position relative for absolute positioning of bin
        chartContainer.style.position = 'relative';

        // Add breathing animation keyframes
        if (!document.getElementById('trash-bin-styles')) {
            const style = document.createElement('style');
            style.id = 'trash-bin-styles';
            style.textContent = `
                @keyframes trashBreathe {
                    0%, 100% { transform: scale(1); }
                    50% { transform: scale(1.08); }
                }
                #chartTrashBin.breathing {
                    animation: trashBreathe 1.5s ease-in-out infinite;
                }
                #chartTrashBin.highlight {
                    animation: none;
                    transform: scale(1.15);
                }
            `;
            document.head.appendChild(style);
        }

        trashBin = document.createElement('div');
        trashBin.id = 'chartTrashBin';
        trashBin.innerHTML = '🚮';
        var posStyle = VoteChart.isPopout
            ? 'top: 10px; right: 10px;'
            : 'bottom: -40px; right: 10px;';
        trashBin.style.cssText = `
            position: absolute;
            ${posStyle}
            font-size: 32px;
            padding: 8px;
            border-radius: 8px;
            background: rgba(220, 53, 69, 0.1);
            border: 2px dashed transparent;
            opacity: 0;
            pointer-events: none;
            transition: opacity 0.2s, border-color 0.2s, background 0.2s;
            z-index: 100;
        `;
        chartContainer.appendChild(trashBin);
    }

    function showTrashBin() {
        if (trashBin) {
            trashBin.style.opacity = '1';
            trashBin.style.pointerEvents = 'auto';
            trashBin.classList.add('breathing');
            trashBin.classList.remove('highlight');
        }
    }

    function hideTrashBin() {
        if (trashBin) {
            trashBin.style.opacity = '0';
            trashBin.style.pointerEvents = 'none';
            trashBin.style.borderColor = 'transparent';
            trashBin.style.background = 'rgba(220, 53, 69, 0.1)';
            trashBin.classList.remove('breathing', 'highlight');
        }
        isOverTrash = false;
    }

    function highlightTrashBin(highlight) {
        if (trashBin) {
            if (highlight) {
                trashBin.style.borderColor = '#dc3545';
                trashBin.style.background = 'rgba(220, 53, 69, 0.3)';
                trashBin.classList.remove('breathing');
                trashBin.classList.add('highlight');
            } else {
                trashBin.style.borderColor = 'transparent';
                trashBin.style.background = 'rgba(220, 53, 69, 0.1)';
                trashBin.classList.add('breathing');
                trashBin.classList.remove('highlight');
            }
        }
        isOverTrash = highlight;
    }

    function isInTrashZone(clientX, clientY) {
        if (!trashBin) return false;
        const rect = trashBin.getBoundingClientRect();
        // Generous hit area
        const padding = 20;
        return clientX >= rect.left - padding && clientX <= rect.right + padding &&
               clientY >= rect.top - padding && clientY <= rect.bottom + padding;
    }

    function isInSafeZone(clientX, clientY) {
        // In pop-out, the entire window is the safe zone
        if (VoteChart.isPopout) return true;

        // Safe zone is a triangular region from bottom-right of canvas toward the trash bin
        // This prevents flyback when dragging toward the bin
        if (!canvas || !trashBin) return false;

        const canvasRect = canvas.getBoundingClientRect();
        const binRect = trashBin.getBoundingClientRect();

        // If we're over the trash, definitely safe
        if (isInTrashZone(clientX, clientY)) return true;

        // Safe zone: below the canvas and to the right of center, moving toward bin
        const isBelow = clientY > canvasRect.bottom - 20;
        const isRightHalf = clientX > canvasRect.left + canvasRect.width / 2;
        const isAboveBinBottom = clientY < binRect.bottom + 50;

        return isBelow && isRightHalf && isAboveBinBottom;
    }

    function trashCategory(label) {
        // Get all original categories if this is a combined group
        const originals = combinedGroups.has(label)
            ? combinedGroups.get(label)
            : [label];

        // Add all originals to trashed set
        originals.forEach(cat => trashedCategories.add(cat));

        // Remove from combined groups if it was one
        if (combinedGroups.has(label)) {
            originals.forEach(cat => categoryToGroup.delete(cat));
            combinedGroups.delete(label);
        }

        // Log to console
        if (typeof window.logToConsole === 'function') {
            window.logToConsole(`🚮 Trashed: ${label}`);
        }

        // Show clear button (so user can restore trashed items)
        const clearBtn = document.getElementById('clearCombinations');
        if (clearBtn) {
            clearBtn.classList.remove('d-none');
        }

        // Refresh chart
        var src = getStatsSource();
        if (src) setData(src);
        render();
    }

    function handleSourceSplitClick() {
        const btn = document.getElementById('sourceSplitBtn');
        const currentlyEnabled = typeof window.sourceTracking !== 'undefined' && window.sourceTracking.isEnabled();
        const newEnabled = !currentlyEnabled;

        if (typeof window.sourceTracking !== 'undefined') {
            window.sourceTracking.setEnabled(newEnabled);
        }

        // Update button glow state
        if (btn) {
            btn.classList.toggle('active-glow', newEnabled);
        }

        // Log to console
        if (typeof window.logToConsole === 'function') {
            window.logToConsole(`Source split: ${newEnabled ? 'ON' : 'OFF'} - ${newEnabled ? 'showing YouTube/Twitch bars' : 'showing combined bars'}`);
        }

        // Update data layout
        var src = getStatsSource();
        if (src) setData(src);

        // Animate the transition
        animateSourceSplit(newEnabled ? 1 : 0);
    }

    function animateSourceSplit(target) {
        animationTarget = target;

        // Cancel any existing animation
        if (animationId) {
            cancelAnimationFrame(animationId);
        }

        const startProgress = animationProgress;
        const startTime = performance.now();

        function animate(currentTime) {
            const elapsed = currentTime - startTime;
            const t = Math.min(elapsed / ANIMATION_DURATION, 1);

            // Ease out cubic for smooth deceleration
            const eased = 1 - Math.pow(1 - t, 3);

            animationProgress = startProgress + (animationTarget - startProgress) * eased;

            render();

            if (t < 1) {
                animationId = requestAnimationFrame(animate);
            } else {
                animationProgress = animationTarget;
                animationId = null;
            }
        }

        animationId = requestAnimationFrame(animate);
    }

    function handleSortChange(e) {
        sortMode = e.target.value;
        saveSortSetting();

        // Log to console
        if (typeof window.logToConsole === 'function') {
            const modeNames = { popular: 'Most Votes', youtube: 'Most YouTube', twitch: 'Most Twitch' };
            window.logToConsole(`Sort: ${modeNames[sortMode] || sortMode}`);
        }

        // Re-render chart with new sort
        var src = getStatsSource();
        if (src) setData(src);
        render();
    }

    function saveSortSetting() {
        try {
            localStorage.setItem('chartSortMode', sortMode);
        } catch (e) {
            // Ignore localStorage errors
        }
    }

    function loadSortSetting() {
        try {
            const saved = localStorage.getItem('chartSortMode');
            if (saved && ['popular', 'youtube', 'twitch'].includes(saved)) {
                sortMode = saved;
                const dropdown = document.getElementById('chartSortDropdown');
                if (dropdown) {
                    dropdown.value = sortMode;
                }
            }
        } catch (e) {
            // Ignore localStorage errors
        }
    }

    function toggleCombineMode() {
        combineMode = !combineMode;

        // Log to console
        if (typeof window.logToConsole === 'function') {
            window.logToConsole(combineMode ? 'Grab mode: ON - click to grab, drop on another to combine' : 'Grab mode: OFF');
        }

        const toggleBtn = document.getElementById('toggleCombineMode');
        if (toggleBtn) {
            toggleBtn.classList.toggle('active', combineMode);
            if (combineMode) {
                toggleBtn.classList.remove('btn-outline-primary');
                toggleBtn.classList.add('btn-primary');
                canvas.style.cursor = 'pointer';
            } else {
                toggleBtn.classList.remove('btn-primary');
                toggleBtn.classList.add('btn-outline-primary');
                canvas.style.cursor = 'default';
                selectedCandidate = null;
                updateSelectedDisplay();
            }
        }

        render();
    }

    function updateSelectedDisplay() {
        const selectedSpan = document.getElementById('combineSelected');
        if (selectedSpan) {
            // Only show the badge in combine mode
            if (combineMode && selectedCandidate) {
                selectedSpan.textContent = `Selected: ${selectedCandidate}`;
                selectedSpan.classList.remove('d-none');
            } else {
                selectedSpan.classList.add('d-none');
            }
        }
    }

    // === DATA MANAGEMENT ===
    function getStatsSource() {
        return _lastStatsMap || (typeof stats !== 'undefined' ? stats : null);
    }

    function setData(statsMap) {
        if (!statsMap) return;

        // Store a local copy so pop-out windows can re-apply combine/trash
        _lastStatsMap = statsMap;

        // Get data source - check for combined stats first
        let dataSource = statsMap;
        if (combinedGroups.size > 0) {
            dataSource = getCombinedStats(statsMap);
        }

        chartData = [];
        dataSource.forEach((voteData, candidate) => {
            // Skip trashed categories
            if (trashedCategories.has(candidate)) return;

            const entry = {
                label: candidate,
                total: 0,
                youtube: 0,
                twitch: 0,
                unknown: 0,
                x: 0, y: 0, width: 0, height: 0
            };

            if (typeof voteData === 'number') {
                entry.total = voteData;
            } else if (typeof voteData === 'object') {
                entry.total = voteData.total || 0;
                entry.youtube = voteData.youtube || 0;
                entry.twitch = voteData.twitch || 0;
                entry.unknown = voteData.unknown || 0;
            }

            // Only include bars with votes
            if (entry.total > 0) {
                chartData.push(entry);
            }
        });

        // Sort based on current sort mode
        chartData.sort((a, b) => {
            let aVal, bVal;
            switch (sortMode) {
                case 'youtube':
                    aVal = a.youtube;
                    bVal = b.youtube;
                    break;
                case 'twitch':
                    aVal = a.twitch;
                    bVal = b.twitch;
                    break;
                case 'popular':
                default:
                    aVal = a.total;
                    bVal = b.total;
                    break;
            }
            // Primary sort by chosen metric, secondary by total
            if (bVal !== aVal) {
                return bVal - aVal;
            }
            return b.total - a.total;
        });

        calculateBarLayout();

        // Broadcast data via BroadcastChannel
        try {
            if (window._chartBroadcast && !_suppressBroadcast) {
                const groups = {};
                combinedGroups.forEach((originals, displayName) => { groups[displayName] = originals; });
                const trashed = Array.from(trashedCategories);

                if (!VoteChart.isPopout) {
                    // Main window -> pop-out: send full stats + combined state
                    const serialized = {};
                    statsMap.forEach((value, key) => { serialized[key] = value; });
                    window._chartBroadcast.postMessage({
                        type: 'chartUpdate',
                        stats: serialized,
                        combinedGroups: groups,
                        trashedCategories: trashed,
                        sourceTrackingEnabled: isSourceTrackingEnabled()
                    });
                } else {
                    // Pop-out -> main window: send combined state changes only
                    window._chartBroadcast.postMessage({
                        type: 'popoutCombineUpdate',
                        combinedGroups: groups,
                        trashedCategories: trashed
                    });
                }
            }
        } catch (e) { }
    }

    function getCombinedStats(baseStats) {
        const result = new Map();
        const groupedCategories = new Set();

        // Track which categories are in groups
        combinedGroups.forEach(originals => {
            originals.forEach(cat => groupedCategories.add(cat));
        });

        // Add combined groups
        combinedGroups.forEach((originals, displayName) => {
            let combinedData = null;

            originals.forEach(originalCat => {
                if (baseStats.has(originalCat)) {
                    const catData = baseStats.get(originalCat);

                    if (typeof catData === 'object') {
                        if (!combinedData) {
                            combinedData = { total: 0, youtube: 0, twitch: 0, unknown: 0 };
                        }
                        combinedData.total += catData.total || 0;
                        combinedData.youtube += catData.youtube || 0;
                        combinedData.twitch += catData.twitch || 0;
                        combinedData.unknown += catData.unknown || 0;
                    } else {
                        if (combinedData === null) {
                            combinedData = 0;
                        }
                        combinedData += catData || 0;
                    }
                }
            });

            if (combinedData !== null) {
                result.set(displayName, combinedData);
            }
        });

        // Add ungrouped categories
        baseStats.forEach((value, key) => {
            if (!groupedCategories.has(key)) {
                if (typeof value === 'object') {
                    result.set(key, { ...value });
                } else {
                    result.set(key, value);
                }
            }
        });

        return result;
    }

    // === LAYOUT CALCULATION ===
    function calculateBarLayout() {
        if (!canvas || chartData.length === 0) return;

        const canvasWidth = canvas.width / dpr;
        const canvasHeight = canvas.height / dpr;

        const chartWidth = canvasWidth - config.padding.left - config.padding.right;
        const chartHeight = canvasHeight - config.padding.top - config.padding.bottom;

        const barCount = chartData.length;
        const totalBarWidth = chartWidth / barCount;
        const spacing = totalBarWidth * config.barSpacing;

        // Calculate combined bar width
        const combinedBarWidth = totalBarWidth * (1 - config.barSpacing);

        // Calculate split bar width (for two sub-bars with gap)
        const splitBarWidth = (totalBarWidth * (1 - config.barSpacing)) / 2 - 1;

        // Max values for both modes - use nice max for grid alignment
        const maxCombined = Math.max(...chartData.map(d => d.total), 1);
        const maxSplit = Math.max(
            ...chartData.map(d => Math.max(d.youtube || 0, d.twitch || 0)),
            1
        );

        // Use niceMax to ensure bars align with gridlines
        const niceCombined = calculateNiceTicks(maxCombined).niceMax;
        const niceSplit = calculateNiceTicks(maxSplit).niceMax;

        chartData.forEach((bar, index) => {
            const groupX = config.padding.left + (index * totalBarWidth) + (spacing / 2);

            // Always calculate combined bar position
            bar.x = groupX;
            bar.width = combinedBarWidth;
            bar.height = (bar.total / niceCombined) * chartHeight;
            bar.y = config.padding.top + chartHeight - bar.height;

            // Always calculate split bar positions (needed for animation)
            bar.ytX = groupX;
            bar.ytWidth = splitBarWidth;
            bar.ytHeight = ((bar.youtube || 0) / niceSplit) * chartHeight;
            bar.ytY = config.padding.top + chartHeight - bar.ytHeight;

            bar.twX = groupX + splitBarWidth + 2; // 2px gap between bars
            bar.twWidth = splitBarWidth;
            bar.twHeight = ((bar.twitch || 0) / niceSplit) * chartHeight;
            bar.twY = config.padding.top + chartHeight - bar.twHeight;

            // Full width when split (for hit testing)
            bar.fullWidth = splitBarWidth * 2 + 2;
        });
    }

    // === LABEL PADDING ADJUSTMENT ===
    // Pre-check if labels need rotation and adjust bottom padding + canvas size accordingly
    function adjustPaddingForLabels() {
        if (!canvas || !ctx || chartData.length === 0) return;

        ctx.font = getFont('label');
        const useSourceTracking = isSourceTrackingEnabled();
        const padding = 8;
        let needsRotation = false;

        // Quick check: would labels overlap or exceed slot width?
        for (let i = 0; i < chartData.length; i++) {
            const bar = chartData[i];
            let slotWidth;
            if (useSourceTracking || animationProgress > 0) {
                slotWidth = bar.twX + bar.twWidth - bar.ytX;
            } else {
                slotWidth = bar.width;
            }
            const labelWidth = ctx.measureText(bar.label).width;
            if (labelWidth > slotWidth + padding) {
                needsRotation = true;
                break;
            }
            if (i < chartData.length - 1) {
                const next = chartData[i + 1];
                let centerX, nextCenterX;
                if (useSourceTracking || animationProgress > 0) {
                    centerX = bar.ytX + (bar.twX + bar.twWidth - bar.ytX) / 2;
                    nextCenterX = next.ytX + (next.twX + next.twWidth - next.ytX) / 2;
                } else {
                    centerX = bar.x + bar.width / 2;
                    nextCenterX = next.x + next.width / 2;
                }
                const nextLabelWidth = ctx.measureText(next.label).width;
                if (centerX + labelWidth / 2 + padding > nextCenterX - nextLabelWidth / 2 - padding) {
                    needsRotation = true;
                    break;
                }
            }
        }

        let targetPadding = DEFAULT_BOTTOM_PADDING;
        if (needsRotation) {
            const maxLabelWidth = Math.min(
                Math.max(...chartData.map(d => ctx.measureText(d.label).width)),
                120
            );
            const verticalExtent = Math.ceil(maxLabelWidth * Math.SQRT1_2) + 20;
            targetPadding = Math.max(DEFAULT_BOTTOM_PADDING, verticalExtent);
        }

        if (config.padding.bottom !== targetPadding) {
            config.padding.bottom = targetPadding;
            calculateBarLayout();
        }
    }

    // === RENDERING ===
    function render() {
        if (!canvas || !ctx) return;

        // Refresh theme colors each render
        config.colors = getThemeColors();

        // Adjust padding/canvas size for rotated labels before drawing
        adjustPaddingForLabels();

        const canvasWidth = canvas.width / dpr;
        const canvasHeight = canvas.height / dpr;

        // Clear canvas
        ctx.clearRect(0, 0, canvasWidth, canvasHeight);

        if (chartData.length === 0) {
            // Draw "No votes yet" message
            ctx.fillStyle = config.colors.text;
            ctx.font = getFont('label');
            ctx.textAlign = 'center';
            ctx.fillText('No votes yet', canvasWidth / 2, canvasHeight / 2);
            return;
        }

        drawGrid();
        drawAxes();
        drawBars();
        drawLabels();

        // Draw legend if source tracking (or animating toward it)
        if (animationProgress > 0) {
            drawLegend(animationProgress);
        }

        // Draw drag-and-drop visualization
        if (isDragging && draggedBar) {
            drawDragVisualization();
        }

        // Draw fly-back animation
        if (flyBackActive && draggedBar) {
            drawFlyBackVisualization();
        }
    }

    function getAnimatedMaxValue() {
        const maxCombined = Math.max(...chartData.map(d => d.total), 1);
        const maxSplit = Math.max(
            ...chartData.map(d => Math.max(d.youtube || 0, d.twitch || 0)),
            1
        );

        // Get nice max values for both modes
        const niceCombined = calculateNiceTicks(maxCombined).niceMax;
        const niceSplit = calculateNiceTicks(maxSplit).niceMax;

        // Interpolate between nice max values during animation
        return lerp(niceCombined, niceSplit, animationProgress);
    }

    // Calculate nice tick values for the y-axis
    function calculateNiceTicks(maxValue) {
        if (maxValue <= 0) return { tickCount: 1, tickStep: 1, niceMax: 1 };

        // For small values, use integer steps
        if (maxValue <= 10) {
            return { tickCount: Math.ceil(maxValue), tickStep: 1, niceMax: Math.ceil(maxValue) };
        }

        // For larger values, find a nice step size
        const roughStep = maxValue / 10;
        const magnitude = Math.pow(10, Math.floor(Math.log10(roughStep)));
        const residual = roughStep / magnitude;

        let niceStep;
        if (residual <= 1.5) niceStep = magnitude;
        else if (residual <= 3) niceStep = 2 * magnitude;
        else if (residual <= 7) niceStep = 5 * magnitude;
        else niceStep = 10 * magnitude;

        const niceMax = Math.ceil(maxValue / niceStep) * niceStep;
        const tickCount = Math.round(niceMax / niceStep);

        return { tickCount, tickStep: niceStep, niceMax };
    }

    function drawGrid() {
        const canvasWidth = canvas.width / dpr;
        const canvasHeight = canvas.height / dpr;
        const chartHeight = canvasHeight - config.padding.top - config.padding.bottom;

        const maxValue = getAnimatedMaxValue();
        const { tickCount, niceMax } = calculateNiceTicks(maxValue);

        ctx.strokeStyle = config.colors.grid;
        ctx.lineWidth = 1;

        for (let i = 0; i <= tickCount; i++) {
            // Position based on actual value relative to niceMax
            const value = (i / tickCount) * niceMax;
            const y = config.padding.top + chartHeight - (value / niceMax * chartHeight);
            ctx.beginPath();
            ctx.moveTo(config.padding.left, y);
            ctx.lineTo(canvasWidth - config.padding.right, y);
            ctx.stroke();
        }
    }

    function drawAxes() {
        const canvasHeight = canvas.height / dpr;
        const chartHeight = canvasHeight - config.padding.top - config.padding.bottom;

        const maxValue = getAnimatedMaxValue();
        const { tickCount, tickStep, niceMax } = calculateNiceTicks(maxValue);

        // Y-axis labels
        ctx.fillStyle = config.colors.axis;
        ctx.font = getFont('axis');
        ctx.textAlign = 'right';
        ctx.textBaseline = 'middle';

        for (let i = 0; i <= tickCount; i++) {
            const value = i * tickStep;
            const y = config.padding.top + chartHeight - (value / niceMax * chartHeight);
            ctx.fillText(value.toString(), config.padding.left - 8, y);
        }
    }

    function drawBars() {
        const useSourceTracking = isSourceTrackingEnabled();
        const isAnimating = animationProgress > 0 && animationProgress < 1;

        chartData.forEach(bar => {
            // Ghost the original bar when it's being dragged
            const isBeingDragged = isDragging && draggedBar && bar.label === draggedBar.label;
            if (isBeingDragged) {
                ctx.save();
                ctx.globalAlpha = 0.3; // Fade the original bar
            }

            if (isAnimating || (useSourceTracking && animationProgress === 1)) {
                drawAnimatedBars(bar, animationProgress);
            } else {
                drawSimpleBar(bar);
            }

            if (isBeingDragged) {
                ctx.restore();
            }

            // Draw selection highlight (works both in and out of combine mode)
            if (selectedCandidate === bar.label) {
                ctx.fillStyle = config.colors.selected;
                ctx.strokeStyle = config.colors.selectedBorder;
                ctx.lineWidth = 2;

                if (useSourceTracking || isAnimating) {
                    // Highlight covers both bars
                    const highlightX = bar.ytX - 2;
                    const highlightY = Math.min(bar.ytY, bar.twY) - 2;
                    const highlightWidth = (bar.twX + bar.twWidth) - bar.ytX + 4;
                    const highlightHeight = Math.max(bar.ytHeight, bar.twHeight) + 4;
                    ctx.fillRect(highlightX, highlightY, highlightWidth, highlightHeight);
                    ctx.strokeRect(highlightX, highlightY, highlightWidth, highlightHeight);
                } else {
                    ctx.fillRect(bar.x - 2, bar.y - 2, bar.width + 4, bar.height + 4);
                    ctx.strokeRect(bar.x - 2, bar.y - 2, bar.width + 4, bar.height + 4);
                }
            }
        });
    }

    function drawSimpleBar(bar) {
        ctx.fillStyle = config.colors.bar;
        ctx.fillRect(bar.x, bar.y, bar.width, bar.height);

        ctx.strokeStyle = config.colors.barBorder;
        ctx.lineWidth = 1;
        ctx.strokeRect(bar.x, bar.y, bar.width, bar.height);

        // Draw vote count on bar
        if (bar.height > 20) {
            ctx.fillStyle = '#fff';
            ctx.font = getFont('value');
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            drawOutlinedText(bar.total.toString(), bar.x + bar.width / 2, bar.y + bar.height / 2);
        }
    }

    function drawAnimatedBars(bar, progress) {
        const canvasHeight = canvas.height / dpr;
        const chartHeight = canvasHeight - config.padding.top - config.padding.bottom;
        const chartBottom = config.padding.top + chartHeight;

        // Get animated max value for proper scaling
        const maxValue = getAnimatedMaxValue();

        // Calculate combined bar dimensions (progress = 0)
        const combinedHeight = (bar.total / maxValue) * chartHeight;
        const combinedY = chartBottom - combinedHeight;

        // Calculate split bar dimensions (progress = 1)
        const ytSplitHeight = ((bar.youtube || 0) / maxValue) * chartHeight;
        const ytSplitY = chartBottom - ytSplitHeight;
        const twSplitHeight = ((bar.twitch || 0) / maxValue) * chartHeight;
        const twSplitY = chartBottom - twSplitHeight;

        // Interpolate widths - from combined (single bar) to split (two bars)
        const halfCombined = bar.width / 2;
        const centerX = bar.x + halfCombined;

        // At progress 0: single bar centered
        // At progress 1: two bars side by side
        const ytX = lerp(bar.x, bar.ytX, progress);
        const ytWidth = lerp(bar.width, bar.ytWidth, progress);
        const twX = lerp(bar.x, bar.twX, progress);
        const twWidth = lerp(bar.width, bar.twWidth, progress);

        // Interpolate heights
        const ytHeight = lerp(combinedHeight, ytSplitHeight, progress);
        const ytY = lerp(combinedY, ytSplitY, progress);
        const twHeight = lerp(combinedHeight, twSplitHeight, progress);
        const twY = lerp(combinedY, twSplitY, progress);

        // Color interpolation - blend from teal to YouTube red / Twitch purple
        const ytColor = lerpColor(config.colors.bar, config.colors.youtube, progress);
        const twColor = lerpColor(config.colors.bar, config.colors.twitch, progress);
        const ytBorder = lerpColor(config.colors.barBorder, config.colors.youtubeBorder, progress);
        const twBorder = lerpColor(config.colors.barBorder, config.colors.twitchBorder, progress);

        // Draw YouTube portion (left)
        if (ytHeight > 0) {
            ctx.fillStyle = ytColor;
            ctx.fillRect(ytX, ytY, ytWidth, ytHeight);
            ctx.strokeStyle = ytBorder;
            ctx.lineWidth = 1;
            ctx.strokeRect(ytX, ytY, ytWidth, ytHeight);

            // Draw count on YouTube bar (only when mostly split)
            if (progress > 0.6 && ytHeight > 20 && bar.youtube > 0) {
                ctx.fillStyle = '#fff';
                ctx.font = getFont('value');
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                drawOutlinedText(bar.youtube.toString(), ytX + ytWidth / 2, ytY + ytHeight / 2);
            }
        }

        // Draw Twitch portion (right) - only when bars are separating
        if (progress > 0.05 && twHeight > 0) {
            ctx.fillStyle = twColor;
            ctx.fillRect(twX, twY, twWidth, twHeight);
            ctx.strokeStyle = twBorder;
            ctx.lineWidth = 1;
            ctx.strokeRect(twX, twY, twWidth, twHeight);

            // Draw count on Twitch bar (only when mostly split)
            if (progress > 0.6 && twHeight > 20 && bar.twitch > 0) {
                ctx.fillStyle = '#fff';
                ctx.font = getFont('value');
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                drawOutlinedText(bar.twitch.toString(), twX + twWidth / 2, twY + twHeight / 2);
            }
        }

        // Draw total count when mostly combined
        if (progress < 0.4 && combinedHeight > 20) {
            ctx.fillStyle = '#fff';
            ctx.font = getFont('value');
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            drawOutlinedText(bar.total.toString(), bar.x + bar.width / 2, combinedY + combinedHeight / 2);
        }
    }

    // Draw text with black outline for readability
    function drawOutlinedText(text, x, y) {
        ctx.strokeStyle = 'rgba(0, 0, 0, 0.7)';
        ctx.lineWidth = 3;
        ctx.lineJoin = 'round';
        ctx.strokeText(text, x, y);
        ctx.fillText(text, x, y);
    }

    // Linear interpolation helper
    function lerp(a, b, t) {
        return a + (b - a) * t;
    }

    // Color interpolation helper (for rgba strings)
    function lerpColor(color1, color2, t) {
        // Parse rgba values
        const c1 = parseRgba(color1);
        const c2 = parseRgba(color2);

        const r = Math.round(lerp(c1.r, c2.r, t));
        const g = Math.round(lerp(c1.g, c2.g, t));
        const b = Math.round(lerp(c1.b, c2.b, t));
        const a = lerp(c1.a, c2.a, t);

        return `rgba(${r}, ${g}, ${b}, ${a})`;
    }

    function parseRgba(color) {
        const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
        if (match) {
            return {
                r: parseInt(match[1]),
                g: parseInt(match[2]),
                b: parseInt(match[3]),
                a: match[4] !== undefined ? parseFloat(match[4]) : 1
            };
        }
        return { r: 75, g: 192, b: 192, a: 0.7 }; // Fallback
    }

    function drawSplitBars(bar) {
        // YouTube bar (left, red)
        if (bar.youtube > 0) {
            ctx.fillStyle = config.colors.youtube;
            ctx.fillRect(bar.ytX, bar.ytY, bar.ytWidth, bar.ytHeight);
            ctx.strokeStyle = config.colors.youtubeBorder;
            ctx.lineWidth = 1;
            ctx.strokeRect(bar.ytX, bar.ytY, bar.ytWidth, bar.ytHeight);

            // Draw count on YouTube bar
            if (bar.ytHeight > 20) {
                ctx.fillStyle = '#fff';
                ctx.font = getFont('value');
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                drawOutlinedText(bar.youtube.toString(), bar.ytX + bar.ytWidth / 2, bar.ytY + bar.ytHeight / 2);
            }
        }

        // Twitch bar (right, purple)
        if (bar.twitch > 0) {
            ctx.fillStyle = config.colors.twitch;
            ctx.fillRect(bar.twX, bar.twY, bar.twWidth, bar.twHeight);
            ctx.strokeStyle = config.colors.twitchBorder;
            ctx.lineWidth = 1;
            ctx.strokeRect(bar.twX, bar.twY, bar.twWidth, bar.twHeight);

            // Draw count on Twitch bar
            if (bar.twHeight > 20) {
                ctx.fillStyle = '#fff';
                ctx.font = getFont('value');
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                drawOutlinedText(bar.twitch.toString(), bar.twX + bar.twWidth / 2, bar.twY + bar.twHeight / 2);
            }
        }
    }

    function drawLabels() {
        const canvasHeight = canvas.height / dpr;
        const useSourceTracking = isSourceTrackingEnabled();

        ctx.fillStyle = config.colors.text;
        ctx.font = getFont('label');

        // Reset hit regions
        labelHitRegions = [];

        // First pass: calculate label positions and widths to detect crowding
        const labelInfo = chartData.map((bar, index) => {
            let labelCenterX, slotWidth;
            if (useSourceTracking || animationProgress > 0) {
                labelCenterX = bar.ytX + (bar.twX + bar.twWidth - bar.ytX) / 2;
                slotWidth = bar.twX + bar.twWidth - bar.ytX;
            } else {
                labelCenterX = bar.x + bar.width / 2;
                slotWidth = bar.width;
            }

            const labelWidth = ctx.measureText(bar.label).width;
            return {
                bar: bar,
                label: bar.label,
                centerX: labelCenterX,
                width: labelWidth,
                slotWidth: slotWidth
            };
        });

        // Check if labels would overlap (with some padding)
        const padding = 8;
        let needsRotation = false;

        for (let i = 0; i < labelInfo.length - 1; i++) {
            const current = labelInfo[i];
            const next = labelInfo[i + 1];

            // Check if the right edge of current label overlaps with left edge of next
            const currentRight = current.centerX + current.width / 2 + padding;
            const nextLeft = next.centerX - next.width / 2 - padding;

            if (currentRight > nextLeft) {
                needsRotation = true;
                break;
            }

            // Also check if label is wider than its slot
            if (current.width > current.slotWidth + padding) {
                needsRotation = true;
                break;
            }
        }

        // Check last label too
        if (labelInfo.length > 0) {
            const last = labelInfo[labelInfo.length - 1];
            if (last.width > last.slotWidth + padding) {
                needsRotation = true;
            }
        }

        const labelY = canvasHeight - config.padding.bottom + 10;
        const labelHeight = getCurrentFontSize() + 4; // Approximate text height with padding

        // Store rotation state for hit detection
        labelsRotated = needsRotation;

        if (needsRotation) {
            // Draw rotated labels at +45 degrees (downward-right) for better visibility
            const rotationAngle = Math.PI / 4; // +45 degrees clockwise

            labelInfo.forEach(info => {
                ctx.save();
                ctx.translate(info.centerX, labelY);
                ctx.rotate(rotationAngle);
                ctx.textAlign = 'left';
                ctx.textBaseline = 'top';

                // Truncate if still too long
                let displayLabel = info.label;
                const maxRotatedWidth = 120;
                let truncateLen = displayLabel.length;
                while (ctx.measureText(displayLabel).width > maxRotatedWidth && truncateLen > 3) {
                    truncateLen--;
                    displayLabel = info.label.substring(0, truncateLen) + '...';
                }

                const actualWidth = ctx.measureText(displayLabel).width;
                ctx.fillText(displayLabel, 0, 0);
                ctx.restore();

                // Store hit region for rotated label
                // Anchor point is at (centerX, labelY), text extends right-downward (left-aligned)
                labelHitRegions.push({
                    bar: info.bar,
                    anchorX: info.centerX,
                    anchorY: labelY,
                    width: actualWidth,
                    height: labelHeight,
                    rotated: true
                });
            });
        } else {
            // Draw horizontal labels (original behavior)
            ctx.textAlign = 'center';
            ctx.textBaseline = 'top';

            labelInfo.forEach(info => {
                // Truncate long labels to fit slot
                let displayLabel = info.label;
                const maxLabelWidth = info.slotWidth + 10;
                let truncateLen = displayLabel.length;
                while (ctx.measureText(displayLabel).width > maxLabelWidth && truncateLen > 3) {
                    truncateLen--;
                    displayLabel = info.label.substring(0, truncateLen) + '...';
                }

                const actualWidth = ctx.measureText(displayLabel).width;
                ctx.fillText(displayLabel, info.centerX, labelY);

                // Store hit region for horizontal label
                labelHitRegions.push({
                    bar: info.bar,
                    anchorX: info.centerX,
                    anchorY: labelY,
                    width: actualWidth,
                    height: labelHeight,
                    rotated: false
                });
            });
        }
    }

    function drawLegend(opacity = 1) {
        const canvasWidth = canvas.width / dpr;
        const legendY = 10;
        const boxSize = 12;
        const gap = 5; // gap between box and text
        const itemGap = 15; // gap between items

        ctx.save();
        ctx.globalAlpha = opacity;

        ctx.font = getFont('axis');
        ctx.textBaseline = 'middle';
        ctx.textAlign = 'left';

        // Measure text widths to position dynamically
        const ytTextWidth = ctx.measureText('YouTube').width;
        const twTextWidth = ctx.measureText('Twitch').width;

        // Right margin to avoid overlapping the pop-out button
        const rightMargin = 45;

        // Calculate positions right-to-left
        const twTextX = canvasWidth - rightMargin - twTextWidth;
        const twBoxX = twTextX - gap - boxSize;
        const ytTextX = twBoxX - itemGap - ytTextWidth;
        const ytBoxX = ytTextX - gap - boxSize;

        // YouTube
        ctx.fillStyle = config.colors.youtube;
        ctx.fillRect(ytBoxX, legendY, boxSize, boxSize);
        ctx.fillStyle = config.colors.text;
        ctx.fillText('YouTube', ytTextX, legendY + boxSize / 2);

        // Twitch
        ctx.fillStyle = config.colors.twitch;
        ctx.fillRect(twBoxX, legendY, boxSize, boxSize);
        ctx.fillStyle = config.colors.text;
        ctx.fillText('Twitch', twTextX, legendY + boxSize / 2);

        ctx.restore();
    }

    function drawDragVisualization() {
        if (!draggedBar) return;

        const canvasHeight = canvas.height / dpr;
        const chartHeight = canvasHeight - config.padding.top - config.padding.bottom;

        // Draw drop target highlight
        if (dropTarget) {
            ctx.save();
            ctx.fillStyle = 'rgba(40, 167, 69, 0.3)'; // Green highlight
            ctx.strokeStyle = 'rgba(40, 167, 69, 0.8)';
            ctx.lineWidth = 3;

            const useSourceTracking = isSourceTrackingEnabled();
            if (useSourceTracking || animationProgress > 0) {
                const highlightX = dropTarget.ytX - 4;
                const highlightY = Math.min(dropTarget.ytY, dropTarget.twY) - 4;
                const highlightWidth = (dropTarget.twX + dropTarget.twWidth) - dropTarget.ytX + 8;
                const highlightHeight = Math.max(dropTarget.ytHeight, dropTarget.twHeight) + 8;
                ctx.fillRect(highlightX, highlightY, highlightWidth, highlightHeight);
                ctx.strokeRect(highlightX, highlightY, highlightWidth, highlightHeight);
            } else {
                ctx.fillRect(dropTarget.x - 4, dropTarget.y - 4, dropTarget.width + 8, dropTarget.height + 8);
                ctx.strokeRect(dropTarget.x - 4, dropTarget.y - 4, dropTarget.width + 8, dropTarget.height + 8);
            }
            ctx.restore();
        }

        // Draw dashed outline around the original bar position
        ctx.save();
        ctx.globalAlpha = 0.8;
        ctx.setLineDash([5, 5]);
        ctx.strokeStyle = config.colors.barBorder;
        ctx.lineWidth = 2;

        const useSourceTracking = isSourceTrackingEnabled();
        if (useSourceTracking || animationProgress > 0) {
            // Outline around both split bars
            const ghostX = draggedBar.ytX;
            const ghostY = Math.min(draggedBar.ytY, draggedBar.twY);
            const ghostWidth = (draggedBar.twX + draggedBar.twWidth) - draggedBar.ytX;
            const ghostHeight = Math.max(draggedBar.ytHeight, draggedBar.twHeight);
            ctx.strokeRect(ghostX, ghostY, ghostWidth, ghostHeight);
        } else {
            // Outline around combined bar
            ctx.strokeRect(draggedBar.x, draggedBar.y, draggedBar.width, draggedBar.height);
        }

        ctx.setLineDash([]);
        ctx.restore();

        // Draw floating bar at cursor position
        const floatWidth = 60;
        const floatHeight = Math.min(80, draggedBar.height || 40);
        const floatX = dragX - floatWidth / 2;
        const floatY = dragY - floatHeight / 2;

        ctx.save();

        // Drop shadow
        ctx.shadowColor = 'rgba(0, 0, 0, 0.4)';
        ctx.shadowBlur = 12;
        ctx.shadowOffsetX = 4;
        ctx.shadowOffsetY = 4;

        // Bar
        ctx.fillStyle = config.colors.bar;
        ctx.fillRect(floatX, floatY, floatWidth, floatHeight);

        ctx.shadowColor = 'transparent';
        ctx.strokeStyle = config.colors.barBorder;
        ctx.lineWidth = 2;
        ctx.strokeRect(floatX, floatY, floatWidth, floatHeight);

        // Label above the floating bar (so cursor doesn't obscure it)
        ctx.fillStyle = '#fff';
        ctx.font = getFont('value');
        ctx.textAlign = 'center';
        ctx.textBaseline = 'bottom';

        // Truncate label if too long
        let label = draggedBar.label;
        const maxLabelWidth = 80;
        while (ctx.measureText(label).width > maxLabelWidth && label.length > 3) {
            label = label.substring(0, label.length - 1);
        }
        if (label !== draggedBar.label) label += '…';

        drawOutlinedText(label, floatX + floatWidth / 2, floatY - 4);

        ctx.restore();

        // Draw instruction hint with background below the floating bar
        ctx.save();
        ctx.font = getFont('axis');
        const hint = dropTarget ? `Drop to combine with "${dropTarget.label}"` : 'Drag onto another bar to combine';
        const hintWidth = ctx.measureText(hint).width;
        const hintX = dragX;
        const hintY = dragY + floatHeight / 2 + 16;
        const padding = 6;
        const textHeight = 14;

        // Draw background pill
        ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
        ctx.beginPath();
        const pillX = hintX - hintWidth / 2 - padding;
        const pillY = hintY - padding / 2;
        const pillWidth = hintWidth + padding * 2;
        const pillHeight = textHeight + padding;
        const radius = 4;
        ctx.roundRect(pillX, pillY, pillWidth, pillHeight, radius);
        ctx.fill();

        // Draw text
        ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'top';
        ctx.fillText(hint, hintX, hintY);
        ctx.restore();
    }

    function drawFlyBackVisualization() {
        if (!draggedBar) return;

        // Interpolate position from start to end
        const currentX = lerp(flyBackStartX, flyBackEndX, flyBackProgress);
        const currentY = lerp(flyBackStartY, flyBackEndY, flyBackProgress);

        // Fade out as it approaches destination
        const opacity = 1 - flyBackProgress;

        // Scale down slightly as it returns
        const scale = lerp(1, 0.8, flyBackProgress);

        const floatWidth = 60 * scale;
        const floatHeight = Math.min(80, draggedBar.height || 40) * scale;
        const floatX = currentX - floatWidth / 2;
        const floatY = currentY - floatHeight / 2;

        ctx.save();
        ctx.globalAlpha = opacity;

        // Drop shadow (fading)
        ctx.shadowColor = `rgba(0, 0, 0, ${0.4 * opacity})`;
        ctx.shadowBlur = 12 * scale;
        ctx.shadowOffsetX = 4 * scale;
        ctx.shadowOffsetY = 4 * scale;

        // Bar
        ctx.fillStyle = config.colors.bar;
        ctx.fillRect(floatX, floatY, floatWidth, floatHeight);

        ctx.shadowColor = 'transparent';
        ctx.strokeStyle = config.colors.barBorder;
        ctx.lineWidth = 2;
        ctx.strokeRect(floatX, floatY, floatWidth, floatHeight);

        // Label above the flying bar
        if (opacity > 0.3) {
            ctx.fillStyle = '#fff';
            ctx.font = getFont('value');
            ctx.textAlign = 'center';
            ctx.textBaseline = 'bottom';

            let label = draggedBar.label;
            const maxLabelWidth = 80;
            while (ctx.measureText(label).width > maxLabelWidth && label.length > 3) {
                label = label.substring(0, label.length - 1);
            }
            if (label !== draggedBar.label) label += '…';

            drawOutlinedText(label, floatX + floatWidth / 2, floatY - 4);
        }

        ctx.restore();
    }

    // === HIT TESTING ===
    function getBarAtPoint(x, y) {
        const useSourceTracking = isSourceTrackingEnabled();

        for (const bar of chartData) {
            if (useSourceTracking) {
                // Check both YouTube and Twitch bar areas
                const inYT = bar.youtube > 0 && x >= bar.ytX && x <= bar.ytX + bar.ytWidth &&
                             y >= bar.ytY && y <= bar.ytY + bar.ytHeight;
                const inTW = bar.twitch > 0 && x >= bar.twX && x <= bar.twX + bar.twWidth &&
                             y >= bar.twY && y <= bar.twY + bar.twHeight;
                if (inYT || inTW) {
                    return bar;
                }
            } else {
                if (x >= bar.x && x <= bar.x + bar.width &&
                    y >= bar.y && y <= bar.y + bar.height) {
                    return bar;
                }
            }
        }

        // Check label hit regions (handles both rotated and horizontal labels)
        for (const region of labelHitRegions) {
            if (region.rotated) {
                // For rotated labels, transform the click point into local coordinates
                // Labels are rotated +45 degrees, so we rotate the point by -45 degrees
                const dx = x - region.anchorX;
                const dy = y - region.anchorY;
                const cos45 = Math.SQRT1_2; // cos(45°) = sin(45°) = √2/2
                const sin45 = Math.SQRT1_2;

                // Rotate point by -45 degrees (inverse of +45 label rotation)
                const rx = dx * cos45 + dy * sin45;
                const ry = -dx * sin45 + dy * cos45;

                // Check if in local bounding box (left-aligned text extends right from anchor)
                const hitPadding = 8;
                if (rx >= -hitPadding && rx <= region.width + hitPadding &&
                    ry >= -hitPadding && ry <= region.height + hitPadding) {
                    return region.bar;
                }
            } else {
                // Horizontal label (center-aligned)
                const hitPadding = 5;
                if (x >= region.anchorX - region.width / 2 - hitPadding &&
                    x <= region.anchorX + region.width / 2 + hitPadding &&
                    y >= region.anchorY - hitPadding &&
                    y <= region.anchorY + region.height + hitPadding) {
                    return region.bar;
                }
            }
        }

        return null;
    }

    // === TOOLTIP ===
    function createTooltip() {
        if (tooltip) return;

        tooltip = document.createElement('div');
        tooltip.id = 'vote-chart-tooltip';
        tooltip.style.cssText = `
            position: fixed;
            background: rgba(0, 0, 0, 0.85);
            color: white;
            padding: 8px 12px;
            border-radius: 4px;
            font-size: 12px;
            pointer-events: none;
            display: none;
            z-index: 1000;
            max-width: 280px;
            line-height: 1.4;
        `;
        document.body.appendChild(tooltip);
    }

    function showTooltip(bar, clientX, clientY) {
        if (!tooltip || !bar) return;

        let html = `<strong>${bar.label}</strong><br>`;
        html += `Total: ${bar.total}`;

        if (isSourceTrackingEnabled()) {
            html += `<br>YouTube: ${bar.youtube} | Twitch: ${bar.twitch}`;
        }

        // Get voter info if available
        if (typeof getVoteUsernames === 'function') {
            // For combined categories, aggregate data
            const originals = combinedGroups.has(bar.label)
                ? combinedGroups.get(bar.label)
                : [bar.label];

            // Get earliest adopter (use group function for combined categories)
            if (typeof getEarliestAdopterForGroup === 'function') {
                const earliest = getEarliestAdopterForGroup(originals);
                if (earliest) {
                    html += `<br><span style="color: #7fff7f;">Earliest Adopter: ${earliest}</span>`;
                }
            } else if (typeof getEarliestAdopter === 'function') {
                // Fallback for backwards compatibility
                const earliest = getEarliestAdopter(originals[0]);
                if (earliest) {
                    html += `<br><span style="color: #7fff7f;">Earliest Adopter: ${earliest}</span>`;
                }
            }

            // Get strongest soldier (only if one-vote-per-user is disabled)
            const oneVoteEnabled = typeof oneVotePerUserConfig !== 'undefined' && oneVotePerUserConfig.enabled;
            if (!oneVoteEnabled) {
                let strongest = null;
                if (typeof getStrongestSoldierForGroup === 'function') {
                    // Use group function to aggregate votes across combined categories
                    strongest = getStrongestSoldierForGroup(originals);
                } else if (typeof getStrongestSoldier === 'function') {
                    // Fallback for backwards compatibility
                    strongest = getStrongestSoldier(originals[0]);
                }
                if (strongest) {
                    html += `<br><span style="color: #ffff7f;">Strongest Soldier: ${strongest.username} (${strongest.count})</span>`;
                }
            }

            // Get recent voters list
            let allVoters = [];
            originals.forEach(cat => {
                const voters = getVoteUsernames(cat);
                allVoters = allVoters.concat(voters);
            });

            if (allVoters.length > 0) {
                const maxDisplay = 10;
                const displayList = allVoters.slice(0, maxDisplay);
                html += `<br><em style="color: #aaa;">Voters: ${displayList.join(', ')}`;
                if (allVoters.length > maxDisplay) {
                    html += ` (+${allVoters.length - maxDisplay} more)`;
                }
                html += '</em>';
            }
        }

        tooltip.innerHTML = html;

        // Position tooltip
        const tooltipRect = tooltip.getBoundingClientRect();
        let left = clientX + 15;
        let top = clientY + 15;

        // Keep tooltip in viewport
        if (left + 280 > window.innerWidth) {
            left = clientX - 290;
        }
        // Ensure tooltip doesn't go off left edge
        if (left < 5) {
            left = 5;
        }
        if (top + tooltipRect.height > window.innerHeight) {
            top = clientY - tooltipRect.height - 15;
        }
        // Ensure tooltip doesn't go off top edge
        if (top < 5) {
            top = 5;
        }

        tooltip.style.left = left + 'px';
        tooltip.style.top = top + 'px';
        tooltip.style.display = 'block';
    }

    function hideTooltip(force = false) {
        // Don't hide if tooltip is locked (unless forced)
        if (tooltipLocked && !force) return;

        if (tooltip) {
            tooltip.style.display = 'none';
        }
        hoveredBar = null;
    }

    function lockTooltip(bar, clientX, clientY) {
        tooltipLocked = true;
        lockedTooltipX = clientX;
        lockedTooltipY = clientY;
        showTooltip(bar, clientX, clientY);
    }

    function unlockTooltip() {
        tooltipLocked = false;
        hideTooltip(true);
    }

    // === EVENT HANDLERS ===
    function cancelHold() {
        if (holdTimer) {
            clearTimeout(holdTimer);
            holdTimer = null;
        }
        if (holdBar && canvas) {
            // Reset cursor when cancelling hold
            canvas.style.cursor = combineMode ? 'pointer' : 'default';
        }
        holdBar = null;
    }

    function handleMouseDown(e) {
        if (!canvas) return;

        // Skip if in cooldown period or already dragging
        if (combineCooldown) return;
        if (isDragging) return;

        const rect = canvas.getBoundingClientRect();
        const x = (e.clientX - rect.left);
        const y = (e.clientY - rect.top);

        const bar = getBarAtPoint(x, y);
        if (bar) {
            // Start the hold timer
            holdStartX = x;
            holdStartY = y;
            holdBar = bar;

            // Change cursor to indicate drag is possible
            canvas.style.cursor = 'grab';

            holdTimer = setTimeout(() => {
                // Hold threshold reached - start dragging
                if (holdBar) {
                    ignoringClick = true; // Prevent click handler from firing after drag
                    startDragging(holdBar, holdStartX, holdStartY);
                    holdBar = null;
                    holdTimer = null;
                }
            }, HOLD_THRESHOLD);
        }
    }

    function handleMouseMove(e) {
        if (!canvas) return;

        const rect = canvas.getBoundingClientRect();
        const x = (e.clientX - rect.left);
        const y = (e.clientY - rect.top);

        // Check if we should cancel the hold timer (mouse moved too far)
        if (holdTimer && holdBar) {
            const dx = x - holdStartX;
            const dy = y - holdStartY;
            if (Math.sqrt(dx * dx + dy * dy) > HOLD_MOVE_TOLERANCE) {
                cancelHold();
            }
        }

        // Handle dragging
        if (isDragging && draggedBar) {
            dragX = x;
            dragY = y;

            // Find potential drop target (any bar except the dragged one)
            const bar = getBarAtPoint(x, y);
            const newDropTarget = (bar && bar.label !== draggedBar.label) ? bar : null;

            if (newDropTarget !== dropTarget) {
                dropTarget = newDropTarget;
            }

            canvas.style.cursor = 'grabbing';
            render();
            return;
        }

        const bar = getBarAtPoint(x, y);

        if (bar) {
            hoveredBar = bar;

            // If a bar is selected, tooltip stays locked - don't update it
            if (!selectedCandidate) {
                // No selection - show tooltip for hovered bar normally
                showTooltip(bar, e.clientX, e.clientY);
            }
            canvas.style.cursor = combineMode ? 'pointer' : 'default';
        } else {
            // Cursor is not on any bar
            if (!selectedCandidate) {
                // No selection - hide tooltip normally
                hideTooltip();
            }
            hoveredBar = null;
            canvas.style.cursor = combineMode ? 'pointer' : 'default';
        }
    }

    function handleClick(e) {
        // Skip click if we just completed a drag (from click-and-hold)
        if (ignoringClick) {
            ignoringClick = false;
            return;
        }

        const rect = canvas.getBoundingClientRect();
        const x = (e.clientX - rect.left);
        const y = (e.clientY - rect.top);

        const bar = getBarAtPoint(x, y);
        if (bar) {
            handleCategoryClick(bar, x, y, e.clientX, e.clientY);
        } else {
            // Clicked on empty space - deselect and unlock tooltip
            if (selectedCandidate) {
                selectedCandidate = null;
                unlockTooltip();
                updateSelectedDisplay();
                render();
            }
        }
    }

    function handleCategoryClick(bar, canvasX, canvasY, clientX, clientY) {
        const candidate = bar.label;

        // In combine mode, click combined groups to split, or drag uncombined bars
        if (combineMode) {
            // Skip if in cooldown period (prevents accidental pickups/splits after combining)
            if (combineCooldown) return;

            // If this is a combined group, split it back apart
            if (combinedGroups.has(candidate)) {
                uncombineCategory(candidate);
                return;
            }

            startDragging(bar, canvasX, canvasY);
            return;
        }

        // Normal mode - toggle selection highlight
        if (selectedCandidate === candidate) {
            // Deselect if clicking same bar
            selectedCandidate = null;
            unlockTooltip();
        } else {
            // Select new bar - lock tooltip at click position
            selectedCandidate = candidate;
            lockTooltip(bar, clientX, clientY);
        }
        updateSelectedDisplay();
        render();
    }

    function startDragging(bar, x, y) {
        // Cancel any fly-back animation in progress
        if (flyBackAnimationId) {
            cancelAnimationFrame(flyBackAnimationId);
            flyBackAnimationId = null;
        }
        flyBackActive = false;

        // Start dragging this bar
        isDragging = true;
        draggedBar = bar;
        dragX = x;
        dragY = y;
        dropTarget = null;

        // Hide tooltip while dragging
        hideTooltip(true);

        // Show trash bin
        showTrashBin();

        // Add global mouse tracking for trash bin detection
        document.addEventListener('mousemove', handleGlobalMouseMove);
        document.addEventListener('mouseup', handleGlobalMouseUp);

        canvas.style.cursor = 'grabbing';
        render();
    }

    function handleGlobalMouseMove(e) {
        if (!isDragging || !draggedBar) return;

        // Update drag position (canvas-relative coordinates)
        // This keeps the dragged bar following the cursor even outside the canvas
        if (canvas) {
            const rect = canvas.getBoundingClientRect();
            dragX = e.clientX - rect.left;
            dragY = e.clientY - rect.top;
            render();
        }

        // Check if over trash bin
        const overTrash = isInTrashZone(e.clientX, e.clientY);
        if (overTrash !== isOverTrash) {
            highlightTrashBin(overTrash);
        }
    }

    function handleGlobalMouseUp(e) {
        if (!isDragging || !draggedBar) {
            cleanupGlobalListeners();
            return;
        }

        // Check if dropped on trash bin
        if (isInTrashZone(e.clientX, e.clientY)) {
            const label = draggedBar.label;

            // Reset drag state first
            isDragging = false;
            draggedBar = null;
            dropTarget = null;
            canvas.style.cursor = 'default';

            // Hide and reset trash bin
            hideTrashBin();
            cleanupGlobalListeners();

            // Trash the category
            trashCategory(label);
            return;
        }

        // If in safe zone but not on trash, start flyback
        if (isInSafeZone(e.clientX, e.clientY)) {
            startFlyBackAnimation();
            hideTrashBin();
            cleanupGlobalListeners();
            return;
        }

        cleanupGlobalListeners();
    }

    function cleanupGlobalListeners() {
        document.removeEventListener('mousemove', handleGlobalMouseMove);
        document.removeEventListener('mouseup', handleGlobalMouseUp);
    }

    function handleDoubleClick(e) {
        if (!canvas) return;

        const rect = canvas.getBoundingClientRect();
        const x = (e.clientX - rect.left);
        const y = (e.clientY - rect.top);

        const bar = getBarAtPoint(x, y);
        if (bar) {
            startDragging(bar, x, y);
        }
    }

    function handleMouseUp(e) {
        // Cancel any pending hold
        cancelHold();

        if (!isDragging || !draggedBar) return;

        const rect = canvas.getBoundingClientRect();
        const x = (e.clientX - rect.left);
        const y = (e.clientY - rect.top);

        // Check if dropped on a valid target
        const bar = getBarAtPoint(x, y);
        if (bar && bar.label !== draggedBar.label) {
            // Combine the dragged bar with the drop target
            combineCategories(draggedBar.label, bar.label);
        }

        // Reset drag state
        isDragging = false;
        draggedBar = null;
        dropTarget = null;
        canvas.style.cursor = 'default';

        // Prevent the subsequent click event from starting a new drag
        ignoringClick = true;

        // Hide trash bin and cleanup
        hideTrashBin();
        cleanupGlobalListeners();

        render();
    }

    function handleMouseLeave(e) {
        // Cancel any pending hold
        cancelHold();

        // If no bar is selected, hide tooltip when cursor leaves
        if (!selectedCandidate) {
            hideTooltip();
        }
        // If a bar is selected, tooltip stays locked - nothing to do

        // If dragging, check if we're heading toward the safe zone (trash bin area)
        // If so, don't flyback - let global handlers manage it
        if (isDragging && draggedBar) {
            // Check if leaving toward the safe zone
            if (e && isInSafeZone(e.clientX, e.clientY)) {
                // Don't flyback - we're heading to trash bin
                return;
            }
            startFlyBackAnimation();
            hideTrashBin();
            cleanupGlobalListeners();
        }
    }

    function startFlyBackAnimation() {
        if (!draggedBar) return;

        // Calculate the center of the original bar position
        const useSourceTracking = isSourceTrackingEnabled();
        if (useSourceTracking || animationProgress > 0) {
            flyBackEndX = draggedBar.ytX + (draggedBar.twX + draggedBar.twWidth - draggedBar.ytX) / 2;
            flyBackEndY = Math.min(draggedBar.ytY, draggedBar.twY) + Math.max(draggedBar.ytHeight, draggedBar.twHeight) / 2;
        } else {
            flyBackEndX = draggedBar.x + draggedBar.width / 2;
            flyBackEndY = draggedBar.y + draggedBar.height / 2;
        }

        flyBackStartX = dragX;
        flyBackStartY = dragY;
        flyBackProgress = 0;
        flyBackActive = true;
        isDragging = false;
        dropTarget = null;
        canvas.style.cursor = 'default';

        // Cancel any existing fly-back animation
        if (flyBackAnimationId) {
            cancelAnimationFrame(flyBackAnimationId);
        }

        const startTime = performance.now();

        function animateFlyBack(currentTime) {
            const elapsed = currentTime - startTime;
            const t = Math.min(elapsed / FLY_BACK_DURATION, 1);

            // Ease out back (slight overshoot for bouncy feel)
            const c1 = 1.70158;
            const c3 = c1 + 1;
            flyBackProgress = 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);

            render();

            if (t < 1) {
                flyBackAnimationId = requestAnimationFrame(animateFlyBack);
            } else {
                // Animation complete - clean up
                flyBackActive = false;
                draggedBar = null;
                flyBackAnimationId = null;
                render();
            }
        }

        flyBackAnimationId = requestAnimationFrame(animateFlyBack);
    }

    function handleKeyDown(e) {
        // Escape key deselects the selected bar and hides tooltip
        if (e.key === 'Escape') {
            if (selectedCandidate || tooltipLocked) {
                selectedCandidate = null;
                unlockTooltip();
                updateSelectedDisplay();
                render();
            }
        }
    }

    function handleResize() {
        if (!canvas) return;

        const container = canvas.parentElement;

        // Temporarily collapse the canvas so it doesn't inflate the container
        canvas.style.height = '0px';
        const rect = container.getBoundingClientRect();

        canvas.width = rect.width * dpr;
        canvas.height = rect.height * dpr;
        canvas.style.width = rect.width + 'px';
        canvas.style.height = rect.height + 'px';

        // Scale context for DPR
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

        calculateBarLayout();
        render();
    }

    // === COMBINE FUNCTIONS ===
    function combineCategories(label1, label2) {
        // Get all original categories from both labels
        const originals1 = combinedGroups.has(label1)
            ? combinedGroups.get(label1)
            : [label1];
        const originals2 = combinedGroups.has(label2)
            ? combinedGroups.get(label2)
            : [label2];

        // Merge all originals
        const allOriginals = [...new Set([...originals1, ...originals2])];
        const newDisplayName = allOriginals.join(' & ');

        // Clean up old groups
        if (combinedGroups.has(label1)) combinedGroups.delete(label1);
        if (combinedGroups.has(label2)) combinedGroups.delete(label2);

        // Remove old reverse lookups
        allOriginals.forEach(cat => categoryToGroup.delete(cat));

        // Create new group
        combinedGroups.set(newDisplayName, allOriginals);
        allOriginals.forEach(cat => categoryToGroup.set(cat, newDisplayName));

        // Log to console
        if (typeof window.logToConsole === 'function') {
            window.logToConsole(`Combined: ${label1} + ${label2} → ${newDisplayName}`);
        }

        // Show clear button
        const clearBtn = document.getElementById('clearCombinations');
        if (clearBtn) {
            clearBtn.classList.remove('d-none');
        }

        // Start cooldown to prevent accidental pickups/splits as bars shift
        combineCooldown = true;
        setTimeout(() => { combineCooldown = false; }, 400);

        // Refresh with updated data
        var src = getStatsSource();
        if (src) setData(src);
        render();
    }

    function uncombineCategory(label) {
        if (!combinedGroups.has(label)) return;

        const originals = combinedGroups.get(label);

        // Remove group and reverse lookups
        combinedGroups.delete(label);
        originals.forEach(cat => categoryToGroup.delete(cat));

        // Log to console
        if (typeof window.logToConsole === 'function') {
            window.logToConsole(`Split apart: ${label} → ${originals.join(', ')}`);
        }

        // Hide clear button if no more combinations or trashed items
        if (combinedGroups.size === 0 && trashedCategories.size === 0) {
            const clearBtn = document.getElementById('clearCombinations');
            if (clearBtn) clearBtn.classList.add('d-none');
        }

        // Cooldown to prevent accidental re-pickup
        combineCooldown = true;
        setTimeout(() => { combineCooldown = false; }, 400);

        // Refresh chart
        var src = getStatsSource();
        if (src) setData(src);
        render();
    }

    function clearCombinations() {
        const hadCombinations = combinedGroups.size > 0;
        const hadTrashed = trashedCategories.size > 0;

        combinedGroups.clear();
        categoryToGroup.clear();
        trashedCategories.clear(); // Also clear trashed categories
        selectedCandidate = null;
        updateSelectedDisplay();

        // Log to console
        if ((hadCombinations || hadTrashed) && typeof window.logToConsole === 'function') {
            window.logToConsole('Cleared all category combinations and trashed items');
        }

        const clearBtn = document.getElementById('clearCombinations');
        if (clearBtn) {
            clearBtn.classList.add('d-none');
        }

        // Refresh with original data
        var src = getStatsSource();
        if (src) setData(src);
        render();
    }

    // === UTILITY ===
    function isSourceTrackingEnabled() {
        return typeof window !== 'undefined' &&
            typeof window.sourceTracking !== 'undefined' &&
            window.sourceTracking.isEnabled();
    }

    function destroy() {
        if (canvas) {
            canvas.removeEventListener('mousedown', handleMouseDown);
            canvas.removeEventListener('mousemove', handleMouseMove);
            canvas.removeEventListener('click', handleClick);
            canvas.removeEventListener('dblclick', handleDoubleClick);
            canvas.removeEventListener('mouseup', handleMouseUp);
            canvas.removeEventListener('mouseleave', handleMouseLeave);
        }
        window.removeEventListener('resize', handleResize);
        document.removeEventListener('keydown', handleKeyDown);

        // Cancel any pending hold timer
        cancelHold();

        // Reset drag state
        isDragging = false;
        draggedBar = null;
        dropTarget = null;

        // Cancel and reset fly-back animation
        if (flyBackAnimationId) {
            cancelAnimationFrame(flyBackAnimationId);
            flyBackAnimationId = null;
        }
        flyBackActive = false;

        if (tooltip && tooltip.parentNode) {
            tooltip.parentNode.removeChild(tooltip);
            tooltip = null;
        }

        // Hide trash bin and cleanup global listeners
        hideTrashBin();
        cleanupGlobalListeners();

        chartData = [];
        combinedGroups.clear();
        categoryToGroup.clear();
        trashedCategories.clear();
        initialized = false; // Allow re-initialization
        lastRenderTime = 0;
        pendingRender = false;
    }

    // === PUBLIC API ===
    return {
        init: init,
        setData: setData,
        render: render,
        isPopout: false, // Set to true on pop-out pages
        suppressBroadcast: function(v) { _suppressBroadcast = v; },
        enableCombineMode: function() {
            if (!combineMode) toggleCombineMode();
        },
        disableCombineMode: function() {
            if (combineMode) toggleCombineMode();
        },
        clearCombinations: clearCombinations,
        hasCombinations: function() {
            return combinedGroups.size > 0 || trashedCategories.size > 0;
        },
        // Sync combined state from main window (used by pop-out)
        setCombinedState: function(groups, trashed) {
            combinedGroups.clear();
            categoryToGroup.clear();
            trashedCategories.clear();
            if (groups) {
                for (const displayName in groups) {
                    const originals = groups[displayName];
                    combinedGroups.set(displayName, originals);
                    originals.forEach(function(cat) { categoryToGroup.set(cat, displayName); });
                }
            }
            if (trashed) {
                trashed.forEach(function(cat) { trashedCategories.add(cat); });
            }
        },
        destroy: destroy
    };
})();

// Expose globally
window.VoteChart = VoteChart;

// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
    setTimeout(function() {
        VoteChart.init('chartjs-chart');
    }, 100);
});
