JIGS Stats

Companion statistics panel for JIGS, with selectable metrics, advanced stats, charts with CI, collapsible sections

当前为 2025-10-29 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         JIGS Stats
// @namespace    http://tampermonkey.net/
// @version      1.0.8
// @description  Companion statistics panel for JIGS, with selectable metrics, advanced stats, charts with CI, collapsible sections
// @author       Jigglymoose & Frotty
// @license      MIT
// @match        https://shykai.github.io/MWICombatSimulatorTest/dist/
// @match        https://shykai.github.io/MWICombatSimulator/dist/
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js
// @run-at       document-idle
// ==/UserScript==

(function() { // <--- Start of main IIFE
    'use strict';

    console.log("JIGS Stats v1.0.8 Loaded");

    // --- CONFIGURATION & STATE VARIABLES ---
    const METRIC_CONFIG = {
        'dpsChange':    { label: 'DPS Δ',            isCostMetric: false, datasetKey: 'dpsChange',        format: 'number', allowZero: true,  chartLabel: 'DPS Change' },
        'gPerDps':      { label: 'G/0.01% DPS',      isCostMetric: true,  datasetKey: 'costPerDps',       format: 'gold',   allowZero: false, chartLabel: 'Gold per 0.01% DPS' },
        'profitChange': { label: 'Profit Δ',         isCostMetric: false, datasetKey: 'profitChange',     format: 'gold',   allowZero: true,  chartLabel: 'Profit Change' },
        'gPerProfit':   { label: 'G/0.01% Profit',   isCostMetric: true,  datasetKey: 'costPerProfit',    format: 'gold',   allowZero: false, chartLabel: 'Gold per 0.01% Profit' },
        'expChange':    { label: 'Exp/Hr Δ',         isCostMetric: false, datasetKey: 'expChange',        format: 'number', allowZero: true,  chartLabel: 'Exp/Hr Change' },
        'gPerExpHr':    { label: 'G/0.01% Exp/Hr',   isCostMetric: true,  datasetKey: 'costPerExp',       format: 'gold',   allowZero: false, chartLabel: 'Gold per 0.01% Exp/Hr' },
        'ephChange':    { label: 'EPH Δ',            isCostMetric: false, datasetKey: 'ephChange',        format: 'number', allowZero: true,  chartLabel: 'EPH Change' },
        'gPerEph':      { label: 'G/0.01% EPH',      isCostMetric: true,  datasetKey: 'costPerEph',       format: 'gold',   allowZero: false, chartLabel: 'Gold per 0.01% EPH' }
    };

    let currentMetric = GM_getValue('jig_rigger_current_metric', 'gPerProfit');
    if (!METRIC_CONFIG[currentMetric] && currentMetric !== 'trueValueSummary') currentMetric = 'gPerProfit';
    let chartInstance = null; let isChartVisible = false; let currentSortKey = null; let currentSortDirection = 1;
    const itemAggregation = new Map(); const lineByLineData = []; let updateCounter = 0;
    let originalPanelPosition = { top: '10px', left: '10px' };
    let isWinsorized = false; // State for Winsorizing
    let isIsolateTrueValue = false; // State for Isolating TV on chart

    // =============================================
    // === FUNCTION DEFINITIONS                  ===
    // =============================================
    function makeDraggable(panel, handle) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; handle.onmousedown = dragMouseDown; function dragMouseDown(e) { e = e || window.event; if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'LABEL') return; e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; if (!panel.style.top && !panel.style.left) { const rect = panel.getBoundingClientRect(); panel.style.top = rect.top + 'px'; panel.style.left = rect.left + 'px'; } document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { e = e || window.event; e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; panel.style.top = (panel.offsetTop - pos2) + "px"; panel.style.left = (panel.offsetLeft - pos1) + "px"; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; const savedPositions = GM_getValue('jig_rigger_panel_position', {}); savedPositions.top = panel.style.top; savedPositions.left = panel.style.left; GM_setValue('jig_rigger_panel_position', savedPositions); } }
    function makeResizable(panel, resizer) { let startX, startY, startWidth, startHeight; resizer.addEventListener('mousedown', initDrag, false); function initDrag(e) { startX = e.clientX; startY = e.clientY; startWidth = parseInt(document.defaultView.getComputedStyle(panel).width, 10); startHeight = parseInt(document.defaultView.getComputedStyle(panel).height, 10); document.documentElement.addEventListener('mousemove', doDrag, false); document.documentElement.addEventListener('mouseup', stopDrag, false); } function doDrag(e) { panel.style.width = (startWidth + e.clientX - startX) + 'px'; panel.style.height = (startHeight + e.clientY - startY) + 'px'; } function stopDrag() { document.documentElement.removeEventListener('mousemove', doDrag, false); document.documentElement.removeEventListener('mouseup', stopDrag, false); const savedPositions = GM_getValue('jig_rigger_panel_position', {}); savedPositions.width = panel.style.width; savedPositions.height = panel.style.height; GM_setValue('jig_rigger_panel_position', savedPositions); } }
    function extractItemName(upgradeText) { return upgradeText; } // Use the full upgrade text

    function parseValueFromDataset(rawValue) {
        if (rawValue === 'N/A' || rawValue === undefined || rawValue === null) return 0; // Treat N/A as 0 for count, but not for stats
        if (rawValue === 'Free') return 0;
        if (rawValue === 'Never' || rawValue === 'Infinity') return Infinity;
        const numValue = parseFloat(rawValue);
        return isNaN(numValue) ? 0 : numValue;
    }

    function isNA(rawValue) {
        return rawValue === 'N/A' || rawValue === undefined || rawValue === null || rawValue === 'Never' || rawValue === 'Infinity';
    }

    // --- Formatting Functions ---
    function formatValue(value, metricKey, allowZeroOverride = null) {
        const config = METRIC_CONFIG[metricKey];
        if (!config) return "N/A";
        const allowZero = allowZeroOverride ?? config.allowZero;

        if (value === null || value === undefined || !isFinite(value)) return 'N/A';
        if (value === 0 && !allowZero) return 'N/A';

        switch(config.format) {
            case 'gold':
                return formatGoldValue(value, allowZero);
            case 'percent':
                return formatPercent(value);
            case 'time':
                return formatTime(value);
            case 'number':
            default:
                return formatNumber(value, 2);
        }
    }
    function formatGoldValue(value, allowZero = false) { if (value === null || value === undefined || !isFinite(value)) return 'N/A'; if (value === 0) return allowZero ? '0' : 'N/A'; if (Math.abs(value) < 1000) return Math.round(value).toLocaleString(); if (Math.abs(value) < 1000000) return `${(value / 1000).toFixed(1)}k`; return `${(value / 1000000).toFixed(2)}M`; }
    function formatNumber(value, decimals = 2) { if (value === null || value === undefined || !isFinite(value)) return 'N/A'; return value.toFixed(decimals); }
    function formatPercent(value) { if (value === null || value === undefined || !isFinite(value)) return 'N/A'; return `${value.toFixed(2)}%`; }
    function formatTime(days) { if (!isFinite(days) || days === Infinity) { return 'Never'; } if (days <= 0) { return 'Free'; } const hours = days * 24; if (hours < 1) { const minutes = hours * 60; return `${minutes.toFixed(0)} min`; } if (days < 1) { return `${hours.toFixed(1)} hrs`; } const months = days / 30.44; if (months >= 1) { return `${months.toFixed(1)} mon`; } return `${days.toFixed(1)} days`; }

    // --- Stat Calculation Functions ---
    function winsorizeData(values, percentile = 0.05) {
        const finiteValues = values.filter(isFinite);
        if (finiteValues.length < 3) return values; // Not enough data to winsorize

        const sorted = [...finiteValues].sort((a, b) => a - b); // Create a new sorted array
        const n = sorted.length;
        const numToClip = Math.ceil(percentile * n);

        if (numToClip === 0 || numToClip * 2 >= n) {
             return values; // Not enough data to clip or would clip everything
        }

        const lowerLimit = sorted[numToClip];
        const upperLimit = sorted[n - 1 - numToClip];

        if (lowerLimit === upperLimit) return values; // All values are the same after clipping bounds

        return values.map(val => {
            if (!isFinite(val)) return val; // Preserve Infinity
            if (val < lowerLimit) return lowerLimit;
            if (val > upperLimit) return upperLimit;
            return val;
        });
    }
    function calculateMedian(values, allowZero) { if (!values || values.length === 0) return 0; const filteredValues = allowZero ? values.filter(isFinite) : values.filter(v => v !== 0 && isFinite(v)); if (filteredValues.length === 0) return 0; const sorted = [...filteredValues].sort((a, b) => a - b); const middle = Math.floor(sorted.length / 2); if (sorted.length % 2 === 0) { return (sorted[middle - 1] + sorted[middle]) / 2; } else { return sorted[middle]; } }
    function calculateStatistics(values, allowZero) { const filteredValues = allowZero ? values.filter(isFinite) : values.filter(v => v !== 0 && isFinite(v)); const n = filteredValues.length; if (n === 0) return { mean: 0, variance: 0, stddev: 0, se: 0, n: 0 }; const mean = filteredValues.reduce((sum, val) => sum + val, 0) / n; if (n === 1) return { mean: mean, variance: 0, stddev: 0, se: 0, n: n }; const variance = filteredValues.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / (n - 1); const stddev = Math.sqrt(variance); const se = stddev / Math.sqrt(n); return { mean: mean, variance: variance, stddev: stddev, se: se, n: n }; }
    function calculateVariancePct(average, min, max) { if (!average && average !== 0) return 'N/A'; if (average === 0) { if (min < 0 && max > 0) return '-∞%/+∞%'; if (min < 0) return '-∞%'; if (max > 0) return '+∞%'; return 'N/A';} if (average < 0) { const minP = ((min - average) / Math.abs(average)) * 100; const maxP = ((max - average) / Math.abs(average)) * 100; return `${minP.toFixed(0)}%/+${maxP.toFixed(0)}%`; } const minP = ((min - average) / average) * 100; const maxP = ((max - average) / average) * 100; return `${minP.toFixed(0)}%/+${maxP.toFixed(0)}%`; }
    function calculateAvgUOVariancePct(average, avgUnder, avgOver) { if (!average && average !== 0) return 'N/A'; if (avgUnder === 0 && avgOver === 0) return 'N/A'; if (average === 0) { if (avgUnder < 0 && avgOver > 0) return '-∞%/+∞%'; if (avgUnder < 0) return '-∞%'; if (avgOver > 0) return '+∞%'; return 'N/A';} if (average < 0) { const underP = avgUnder !== 0 ? ((avgUnder - average) / Math.abs(average)) * 100 : 0; const overP = avgOver !== 0 ? ((avgOver - average) / Math.abs(average)) * 100 : 0; return `${underP.toFixed(0)}%/+${overP.toFixed(0)}%`; } const underP = avgUnder !== 0 ? ((avgUnder - average) / average) * 100 : 0; const overP = avgOver !== 0 ? ((avgOver - average) / average) * 100 : 0; return `${underP.toFixed(0)}%/+${overP.toFixed(0)}%`; }

    // --- Panel State & Data Update Functions ---
    function applySavedPanelState() { const savedPosition = GM_getValue('jig_rigger_panel_position'); const riggerPanelElement = document.getElementById('jig-rigger-panel'); if (riggerPanelElement) { if (savedPosition) { if (savedPosition.top && savedPosition.left) { riggerPanelElement.style.top = savedPosition.top; riggerPanelElement.style.left = savedPosition.left; originalPanelPosition = { top: savedPosition.top, left: savedPosition.left }; } if (savedPosition.width) riggerPanelElement.style.width = savedPosition.width; if (savedPosition.height) riggerPanelElement.style.height = savedPosition.height; } const isMinimized = GM_getValue('jig_rigger_minimized', false); if (isMinimized) { riggerPanelElement.classList.add('jig-rigger-minimized'); const toggleButton = document.getElementById('rigger-toggle'); if (toggleButton) toggleButton.textContent = '+'; } } isChartVisible = GM_getValue('jig_rigger_chart_visible', false); const isAggregatedCollapsed = GM_getValue('jig_rigger_aggregated_collapsed', false); const aggSection = document.getElementById('aggregated-section'); const aggToggle = document.getElementById('aggregated-toggle'); if (aggSection && aggToggle) { if (isAggregatedCollapsed) { aggSection.classList.add('collapsed'); aggToggle.textContent = '+'; } else { aggSection.classList.remove('collapsed'); aggToggle.textContent = '-'; } } const isLineByLineCollapsed = GM_getValue('jig_rigger_line_by_line_collapsed', false); const lineSection = document.getElementById('line-by-line-section'); const lineToggle = document.getElementById('line-by-line-toggle'); if(lineSection && lineToggle) { if (isLineByLineCollapsed) { lineSection.classList.add('collapsed'); lineToggle.textContent = '+'; } else { lineSection.classList.remove('collapsed'); lineToggle.textContent = '-'; } } isWinsorized = GM_getValue('jig_rigger_winsorized', false); const winsorizeCheckbox = document.getElementById('jr-winsorize-checkbox'); if (winsorizeCheckbox) winsorizeCheckbox.checked = isWinsorized;
        isIsolateTrueValue = GM_getValue('jig_rigger_isolate_tv', false);
        const isolateCheckbox = document.getElementById('jr-isolate-tv-checkbox');
        if (isolateCheckbox) isolateCheckbox.checked = isIsolateTrueValue;

        updateTableHeaders();
    }
    function updateAggregation(itemName, trElement) { if (!itemAggregation.has(itemName)) itemAggregation.set(itemName, new Map()); const itemMetrics = itemAggregation.get(itemName); const lineEntryStats = {}; for (const metricKey in METRIC_CONFIG) { const config = METRIC_CONFIG[metricKey]; const rawValue = trElement.dataset[config.datasetKey]; const valueIsNA = isNA(rawValue); const parsedValue = parseValueFromDataset(rawValue); if (!itemMetrics.has(metricKey)) itemMetrics.set(metricKey, { count: 0, naCount: 0, values: [] }); const metricData = itemMetrics.get(metricKey); metricData.count++; if (valueIsNA) metricData.naCount++; metricData.values.push(parsedValue); const valuesToProcess = isWinsorized ? winsorizeData(metricData.values, 0.05) : metricData.values; const total = valuesToProcess.reduce((sum, val) => sum + (isFinite(val) ? val : 0), 0); const avg = metricData.count > 0 ? total / metricData.count : 0; const useZerosForStats = config.allowZero; const relevantValues = useZerosForStats ? valuesToProcess.filter(isFinite) : valuesToProcess.filter(v => v !== 0 && isFinite(v)); const median = calculateMedian(valuesToProcess, useZerosForStats); const stats = calculateStatistics(relevantValues, useZerosForStats); const min = relevantValues.length > 0 ? Math.min(...relevantValues) : 0; const max = relevantValues.length > 0 ? Math.max(...relevantValues) : 0; const valuesUnder = relevantValues.filter(v => v < stats.mean); const valuesOver = relevantValues.filter(v => v > stats.mean); const avgUnder = valuesUnder.length > 0 ? valuesUnder.reduce((sum, val) => sum + val, 0) / valuesUnder.length : 0; const avgOver = valuesOver.length > 0 ? valuesOver.reduce((sum, val) => sum + val, 0) / valuesOver.length : 0; const ci_lower = (stats.n > 1) ? stats.mean - (1.96 * stats.se) : 0; const ci_upper = (stats.n > 1) ? stats.mean + (1.96 * stats.se) : 0; const tStat = (stats.n > 1 && stats.se > 0) ? stats.mean / stats.se : 0;

        const hasValidCi = stats.n > 1;
        let trueValue = null;
        const calculatedTv = (avg + median) / 2;
        if (hasValidCi && calculatedTv >= ci_lower && calculatedTv <= ci_upper) {
            trueValue = calculatedTv;
        }

        lineEntryStats[metricKey] = {
            count: metricData.count,
            naCount: metricData.naCount,
            total,
            avg,
            median,
            stddev: stats.stddev,
            ci_lower,
            ci_upper,
            trueValue,
            tStat,
            min,
            max,
            avgUnder,
            avgOver,
            minMaxVariance: calculateVariancePct(avg, min, max),
            avgUOVariance: calculateAvgUOVariancePct(avg, avgUnder, avgOver)
        };
    }
    updateCounter++; const timestamp = new Date().toLocaleTimeString(); lineByLineData.push({ id: updateCounter, timestamp, itemName, stats: lineEntryStats }); updateRiggerTable(); updateLineByLineTable(); }

    function updateTableHeaders() {
        const aggTable = document.getElementById('rigger-results-table');
        const aggTHeadTr = aggTable ? aggTable.querySelector('thead tr') : null;
        const lineTHeadTr = document.querySelector('#line-by-line-table thead tr');
        const lineByLineSection = document.getElementById('line-by-line-section');
        const chartContainer = document.getElementById('jr_chart-container');
        const isolateTvLabel = document.getElementById('jr-isolate-tv-label');
        const aggSection = document.getElementById('aggregated-section');
        const aggContainer = document.getElementById('rigger-results-container');
        const rankLedger = document.getElementById('jigs-rank-ledger'); // <-- NEW

        if (!aggTHeadTr || !lineByLineSection || !chartContainer || !isolateTvLabel || !lineTHeadTr || !aggSection || !aggContainer || !rankLedger) return;

        if (currentMetric === 'trueValueSummary') {
            // Build summary table headers
            let headers = '<th data-sort-key="name">Item Name</th><th data-sort-key="count">#</th><th data-sort-key="naCount">N/A</th>';
            for (const key in METRIC_CONFIG) {
                headers += `<th data-sort-key="${key}" title="${METRIC_CONFIG[key].label}">${METRIC_CONFIG[key].label}</th>`;
            }
            aggTHeadTr.innerHTML = headers;
            aggTable.classList.add('jigs-summary-table');
            aggTable.classList.remove('jigs-metric-table');

            // Hide/Show UI elements
            lineByLineSection.style.display = 'none';
            chartContainer.style.display = 'none';
            isolateTvLabel.style.display = 'none';
            rankLedger.style.display = 'flex'; // <-- NEW

            aggSection.style.flexGrow = '1';
            aggContainer.style.maxHeight = 'none';

            // Update chart canvas with message
            if (chartInstance) { chartInstance.destroy(); chartInstance = null; }
            const canvas = document.getElementById('jr_chart-canvas');
            if (canvas) {
                const ctx = canvas.getContext('2d');
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                ctx.fillStyle = '#aaa'; ctx.font = '16px sans-serif'; ctx.textAlign = 'center';
                ctx.fillText('Chart is disabled for "True Value" summary view', canvas.width / 2, canvas.height / 2 - 10);
                ctx.fillText('due to incompatible Y-axis scales.', canvas.width / 2, canvas.height / 2 + 10);
            }

        } else {
            // Build standard table headers
            aggTHeadTr.innerHTML = `
                <th data-sort-key="name" title="The name of the item being upgraded.">Item Name</th>
                <th data-sort-key="count" title="The total number of times this item has appeared in the simulation results.">#</th>
                <th data-sort-key="naCount" title="The number of times this item's value was 'N/A' or 'Free' for the selected metric.">N/A</th>
                <th data-sort-key="trueValue" title="Median of the Avg and Median, *if* the calculated TV is within the 95% CI. (Avg+Median)/2">True Value</th>
                <th data-sort-key="total" title="The sum of all values for this item for the selected metric.">Total</th>
                <th data-sort-key="avg" title="The average value (arithmetic mean) including N/A (as 0) entries. (Total / #)">Avg</th>
                <th data-sort-key="median" title="The *median* value (50th percentile) excluding N/A (0) entries. Good measure of the 'typical' value.">Median</th>
                <th data-sort-key="stddev" title="Standard Deviation: Square root of Variance. Measures typical deviation from the average, in the original units. (Math: √Variance)">Std Dev</th>
                <th data-sort-key="ci" title="95% Confidence Interval: We are 95% confident the *true* average lies within this range. (Math: Avg ± 1.96 * StdErr)">95% CI</th>
                <th data-sort-key="tStat" title="T-Statistic: Tests if the average value is statistically different from zero. Absolute value > ~2 is generally significant. (Math: Avg / StdErr)">T-Stat</th>
                <th data-sort-key="min" title="The lowest value recorded for this item (excluding N/A=0 unless metric allows 0).">Min</th>
                <th data-sort-key="max" title="The highest value recorded for this item.">Max</th>
                <th data-sort-key="minMaxVariance" title="Percentage difference of Min/Max from the Avg.">Min/Max %Var</th>
                <th data-sort-key="avgUnder" title="The average of values *below* the main Avg.">Avg Under</th>
                <th data-sort-key="avgOver" title="The average of values *above* the main Avg.">Avg Over</th>
                <th data-sort-key="avgUOVariance" title="Percentage difference of Avg Under/Over from the main Avg.">Avg U/O %Var</th>
            `;
            aggTable.classList.remove('jigs-summary-table');
            aggTable.classList.add('jigs-metric-table');

            // Build line-by-line headers
            lineTHeadTr.innerHTML = `
                <th title="Update counter.">#</th> <th title="Timestamp of the update.">Timestamp</th> <th title="Item name.">Item Name</th> <th title="Cumulative count for this item.">#</th> <th title="Cumulative N/A count for the selected metric.">N/A</th> <th title="Median of the Avg and Median, *if* the calculated TV is within the 95% CI. (Avg+Median)/2">True Value</th> <th title="Cumulative total value for the selected metric.">Total</th> <th title="Cumulative average value.">Avg</th> <th title="Cumulative median value.">Median</th> <th title="Cumulative standard deviation.">Std Dev</th> <th title="Cumulative 95% CI.">95% CI</th> <th title="Cumulative T-Statistic.">T-Stat</th> <th title="Cumulative minimum value.">Min</th> <th title="Cumulative maximum value.">Max</th> <th title="Cumulative Min/Max % Variance.">Min/Max %Var</th> <th title="Cumulative Avg Under.">Avg Under</th> <th title="Cumulative Avg Over.">Avg Over</th> <th title="Cumulative Avg U/O % Variance.">Avg U/O %Var</th>
            `;

            // Hide/Show UI elements
            lineByLineSection.style.display = 'flex';
            isolateTvLabel.style.display = 'block';
            rankLedger.style.display = 'none'; // <-- NEW

            aggSection.style.flexGrow = '0';
            aggContainer.style.maxHeight = '250px';

            if (isChartVisible) {
                chartContainer.style.display = 'block';
            }
        }
    }

function buildTrueValueSummaryTable() {
        const tbody = document.querySelector('#rigger-results-table tbody');
        tbody.innerHTML = '';

        let itemsArray = Array.from(itemAggregation.entries()).map(([itemName, itemMetrics]) => {
            const itemData = { name: itemName };
            const firstMetricKey = Object.keys(METRIC_CONFIG)[0];
            const defaultMetricData = itemMetrics.get('gPerProfit') || itemMetrics.get(firstMetricKey);

            if (!defaultMetricData) return null;
            itemData.count = defaultMetricData.count;
            itemData.naCount = defaultMetricData.naCount;

            for (const metricKey in METRIC_CONFIG) {
                const config = METRIC_CONFIG[metricKey];
                const metricData = itemMetrics.get(metricKey);

                if (!metricData) {
                    itemData[metricKey] = null; // for sorting
                    itemData[metricKey + '_text'] = 'N/A';
                    continue;
                }

                const useZerosForStats = config.allowZero;
                const valuesToProcess = isWinsorized ? winsorizeData(metricData.values, 0.05) : metricData.values;
                const relevantValues = useZerosForStats ? valuesToProcess.filter(isFinite) : valuesToProcess.filter(v => v !== 0 && isFinite(v));
                const total = valuesToProcess.reduce((sum, val) => sum + (isFinite(val) ? val : 0), 0);
                const avg = metricData.count > 0 ? total / metricData.count : 0;
                const median = calculateMedian(valuesToProcess, useZerosForStats);
                const stats = calculateStatistics(relevantValues, useZerosForStats);

                const ci_lower = (stats.n > 1) ? stats.mean - (1.96 * stats.se) : 0;
                const ci_upper = (stats.n > 1) ? stats.mean + (1.96 * stats.se) : 0;

                const hasValidCi = stats.n > 1;
                let trueValue = null;
                let trueValueText;
                const calculatedTv = (avg + median) / 2;

                if (hasValidCi && calculatedTv >= ci_lower && calculatedTv <= ci_upper) {
                    trueValue = calculatedTv;
                    trueValueText = formatValue(trueValue, metricKey, true);
                } else {
                    trueValueText = 'Not Enough Data';
                }
                itemData[metricKey] = trueValue;
                itemData[metricKey + '_text'] = trueValueText;
            }
            return itemData;
        }).filter(item => item !== null);

        // Find column extremes for highlighting
        let columnExtremes = {};
        for (const metricKey in METRIC_CONFIG) {
            const config = METRIC_CONFIG[metricKey];
            const validValues = itemsArray.map(item => item[metricKey]).filter(v => v !== null && isFinite(v));

            if (validValues.length < 2) continue;

            const minVal = Math.min(...validValues);
            const maxVal = Math.max(...validValues);

            if (minVal === maxVal) continue;

            if (config.isCostMetric) {
                columnExtremes[metricKey] = { best: minVal, worst: maxVal }; // Lower is better
            } else {
                columnExtremes[metricKey] = { best: maxVal, worst: minVal }; // Higher is better
            }
        }

        // --- Calculate bestCount for each item ---
        itemsArray.forEach(item => {
            item.bestCount = 0;
            for (const metricKey in METRIC_CONFIG) {
                const extremes = columnExtremes[metricKey];
                if (extremes && item[metricKey] === extremes.best) {
                    item.bestCount++;
                }
            }
        });

        // --- *** UPDATED LOGIC *** ---
        // Find the top 5 items *that have at least one green box*
        const rankedItems = [...itemsArray]
            .filter(item => item.bestCount > 0) // Filter for items with bestCount > 0
            .sort((a, b) => b.bestCount - a.bestCount); // Then sort

        // Get the names of the top 5 from this filtered list
        const topItemNames = rankedItems.slice(0, 5).map(item => item.name);
        // --- *** END UPDATED LOGIC *** ---


        // Sort
        if (currentSortKey) {
             itemsArray.sort((a, b) => {
                 if (currentSortKey === 'name' || currentSortKey === 'count' || currentSortKey === 'naCount') {
                      let valA = a[currentSortKey];
                      let valB = b[currentSortKey];
                      if(currentSortKey === 'name') return valA.localeCompare(valB) * currentSortDirection;
                      return (valA - valB) * currentSortDirection;
                 }
                 let valA = a[currentSortKey];
                 let valB = b[currentSortKey];
                 let valA_text = a[currentSortKey + '_text'];
                 let valB_text = b[currentSortKey + '_text'];
                 if (valA_text === 'Not Enough Data') return 1 * currentSortDirection;
                 if (valB_text === 'Not Enough Data') return -1 * currentSortDirection;
                 if (valA === null) return 1 * currentSortDirection;
                 if (valB === null) return -1 * currentSortDirection;

                 return (valA - valB) * currentSortDirection;
             });
        } else {
            itemsArray.sort((a, b) => a.name.localeCompare(b.name)); // Default sort by name
        }

        // Render
        for (const item of itemsArray) {
            const row = tbody.insertRow();

            // This logic now works correctly because topItemNames only contains ranked items
            const rank = topItemNames.indexOf(item.name);
            let rankClass = (rank !== -1) ? ` jigs-rank-${rank + 1}` : '';
            let rowHTML = `<td class="${rankClass}">${item.name}</td><td>${item.count}</td><td>${item.naCount}</td>`;

            for (const metricKey in METRIC_CONFIG) {
                let className = '';
                const extremes = columnExtremes[metricKey];
                const val = item[metricKey];

                if (extremes && val !== null && isFinite(val)) {
                    if (val === extremes.best) className = 'jigs-tv-best';
                    else if (val === extremes.worst) className = 'jigs-tv-worst';
                }
                rowHTML += `<td class="${className}">${item[metricKey + '_text']}</td>`;
            }
            row.innerHTML = rowHTML;
        }
    }

    function updateRiggerTable() {
        if (currentMetric === 'trueValueSummary') {
            buildTrueValueSummaryTable();
            return;
        }

        const tbody = document.querySelector('#rigger-results-table tbody');
        tbody.innerHTML = '';
        const config = METRIC_CONFIG[currentMetric];
        let itemsArray = Array.from(itemAggregation.entries()).map(([itemName, itemMetrics]) => { const metricData = itemMetrics.get(currentMetric); if (!metricData || metricData.values.length === 0) return null; const useZerosForStats = config.allowZero; const valuesToProcess = isWinsorized ? winsorizeData(metricData.values, 0.05) : metricData.values; const relevantValues = useZerosForStats ? valuesToProcess.filter(isFinite) : valuesToProcess.filter(v => v !== 0 && isFinite(v)); const total = valuesToProcess.reduce((sum, val) => sum + (isFinite(val) ? val : 0), 0); const avg = metricData.count > 0 ? total / metricData.count : 0; const median = calculateMedian(valuesToProcess, useZerosForStats); const stats = calculateStatistics(relevantValues, useZerosForStats); if (!stats) { console.warn(`Stats calculation failed for ${itemName}, metric ${currentMetric}`); return null; } const min = relevantValues.length > 0 ? Math.min(...relevantValues) : 0; const max = relevantValues.length > 0 ? Math.max(...relevantValues) : 0; const valuesUnder = relevantValues.filter(v => v < stats.mean); const valuesOver = relevantValues.filter(v => v > stats.mean); const avgUnder = valuesUnder.length > 0 ? valuesUnder.reduce((sum, val) => sum + val, 0) / valuesUnder.length : 0; const avgOver = valuesOver.length > 0 ? valuesOver.reduce((sum, val) => sum + val, 0) / valuesOver.length : 0; const ci_lower = (stats.n > 1) ? stats.mean - (1.96 * stats.se) : 0; const ci_upper = (stats.n > 1) ? stats.mean + (1.96 * stats.se) : 0; const confidenceInterval = (stats.n > 1) ? `${formatValue(ci_lower, currentMetric, true)} - ${formatValue(ci_upper, currentMetric, true)}` : 'N/A'; const tStat = (stats.n > 1 && stats.se > 0) ? stats.mean / stats.se : 0;

        const hasValidCi = stats.n > 1;
        let trueValue = null;
        let trueValueText;
        const calculatedTv = (avg + median) / 2;

        if (hasValidCi && calculatedTv >= ci_lower && calculatedTv <= ci_upper) {
            trueValue = calculatedTv;
            trueValueText = formatValue(trueValue, currentMetric, true);
        } else {
            trueValueText = 'Not Enough Data';
        }

        return {
            name: itemName,
            count: metricData.count,
            naCount: metricData.naCount,
            trueValue,
            trueValueText,
            total,
            avg,
            median,
            stddev: stats.stddev,
            ci: confidenceInterval,
            tStat,
            min,
            max,
            avgUnder,
            avgOver,
            minMaxVariance: calculateVariancePct(avg, min, max),
            avgUOVariance: calculateAvgUOVariancePct(avg, avgUnder, avgOver)
        };
    }).filter(item => item !== null); if (currentSortKey) { itemsArray.sort((a, b) => { let valA = a[currentSortKey]; let valB = b[currentSortKey]; if (typeof valA === 'string') { if (valA === 'N/A' || valA.includes('N/A') || valA === 'Not Enough Data') return 1 * currentSortDirection; if (valB === 'N/A' || valB.includes('N/A') || valB === 'Not Enough Data') return -1 * currentSortDirection; return valA.localeCompare(valB) * currentSortDirection; } if (valA === null) return 1 * currentSortDirection; if (valB === null) return -1 * currentSortDirection; return (valA - valB) * currentSortDirection; }); } else { itemsArray.sort((a, b) => a.name.localeCompare(b.name)); } for (const item of itemsArray) { const row = tbody.insertRow(); row.innerHTML = `<td>${item.name ?? 'N/A'}</td><td>${item.count ?? 'N/A'}</td><td>${item.naCount ?? 'N/A'}</td><td>${item.trueValueText}</td><td>${formatValue(item.total, currentMetric, true)}</td><td>${formatValue(item.avg, currentMetric, true)}</td><td>${formatValue(item.median, currentMetric, config.allowZero)}</td><td>${formatValue(item.stddev, currentMetric, true)}</td><td>${item.ci ?? 'N/A'}</td><td>${formatNumber(item.tStat, 2)}</td><td>${formatValue(item.min, currentMetric, config.allowZero)}</td><td>${formatValue(item.max, currentMetric, true)}</td><td>${item.minMaxVariance ?? 'N/A'}</td><td>${formatValue(item.avgUnder, currentMetric, true)}</td><td>${formatValue(item.avgOver, currentMetric, true)}</td><td>${item.avgUOVariance ?? 'N/A'}</td>`; } if (isChartVisible) updateChart(); }

    function updateLineByLineTable(redrawAll = false) {
        if (currentMetric === 'trueValueSummary') return;

        const tbody = document.querySelector('#line-by-line-table tbody');
        if (redrawAll) { tbody.innerHTML = ''; for (let i = lineByLineData.length - 1; i >= 0; i--) { addLineByLineRow(tbody, lineByLineData[i]); } } else if (lineByLineData.length > 0) { addLineByLineRow(tbody, lineByLineData[lineByLineData.length - 1], true); }
    }

    function addLineByLineRow(tbody, lineData, insertAtTop = false){ if (!lineData || !lineData.stats) { console.warn("JIGS Stats: addLineByLineRow called with invalid lineData:", lineData); return; } const stats = lineData.stats[currentMetric]; if (!stats || typeof stats.min === 'undefined') { console.warn(`JIGS Stats: addLineByLineRow - stats missing or invalid for metric ${currentMetric} in item ${lineData.itemName}`); return; }
        const config = METRIC_CONFIG[currentMetric];

        let trueValueText;
        if (stats.trueValue !== null) {
            trueValueText = formatValue(stats.trueValue, currentMetric, true);
        } else {
            trueValueText = 'Not Enough Data';
        }

        const row = insertAtTop ? tbody.insertRow(0) : tbody.insertRow();
        const confidenceInterval = (stats.ci_lower !== undefined && stats.ci_upper !== undefined) ? `${formatValue(stats.ci_lower, currentMetric, true)} - ${formatValue(stats.ci_upper, currentMetric, true)}` : 'N/A'; const minMaxVarText = stats.minMaxVariance !== undefined ? stats.minMaxVariance : 'N/A'; const avgUOVarText = stats.avgUOVariance !== undefined ? stats.avgUOVariance : 'N/A';
        row.innerHTML = `<td>${lineData.id}</td><td>${lineData.timestamp}</td><td>${lineData.itemName}</td><td>${stats.count ?? 'N/A'}</td><td>${stats.naCount ?? 'N/A'}</td><td>${trueValueText}</td><td>${formatValue(stats.total, currentMetric, true)}</td><td>${formatValue(stats.avg, currentMetric, true)}</td><td>${formatValue(stats.median, currentMetric, config.allowZero)}</td><td>${formatValue(stats.stddev, currentMetric, true)}</td><td>${confidenceInterval}</td><td>${formatNumber(stats.tStat, 2)}</td><td>${formatValue(stats.min, currentMetric, config.allowZero)}</td><td>${formatValue(stats.max, currentMetric, true)}</td><td>${minMaxVarText}</td><td>${formatValue(stats.avgUnder, currentMetric, true)}</td><td>${formatValue(stats.avgOver, currentMetric, true)}</td><td>${avgUOVarText}</td>`;
    }
    function clearRiggerData() { itemAggregation.clear(); lineByLineData.length = 0; updateCounter = 0; updateRiggerTable(); document.querySelector('#line-by-line-table tbody').innerHTML = ''; if (isChartVisible) updateChart(); }

    function updateChart() {
        if (currentMetric === 'trueValueSummary') {
            updateTableHeaders();
            return;
        }

        const canvas = document.getElementById('jr_chart-canvas');
        const ctx = canvas.getContext('2d');
        const config = METRIC_CONFIG[currentMetric];
        const itemsArray = Array.from(itemAggregation.entries()).map(([itemName, itemMetrics]) => {
            const metricData = itemMetrics.get(currentMetric);
            if (!metricData) return null;
            const valuesToProcess = isWinsorized ? winsorizeData(metricData.values, 0.05) : metricData.values;
            const useZerosForStats = config.allowZero;
            const relevantValues = useZerosForStats ? valuesToProcess.filter(isFinite) : valuesToProcess.filter(v => v !== 0 && isFinite(v));
            const stats = calculateStatistics(relevantValues, useZerosForStats);
            if(!stats) return null;
            const avg = metricData.count > 0 ? valuesToProcess.reduce((sum, val) => sum + (isFinite(val) ? val : 0), 0) / metricData.count : 0;
            const median = calculateMedian(valuesToProcess, useZerosForStats);
            const min = relevantValues.length > 0 ? Math.min(...relevantValues) : 0;
            const max = relevantValues.length > 0 ? Math.max(...relevantValues) : 0;
            const ci_lower = (stats.n > 1) ? stats.mean - (1.96 * stats.se) : 0;
            const ci_upper = (stats.n > 1) ? stats.mean + (1.96 * stats.se) : 0;

            const hasValidCi = stats.n > 1;
            let trueValue = null;
            const calculatedTv = (avg + median) / 2;

            if (hasValidCi && calculatedTv >= ci_lower && calculatedTv <= ci_upper) {
                trueValue = calculatedTv;
            }

            return { name: itemName, min, max, avg, median, ci_lower, ci_upper, n: stats.n, trueValue };
        }).filter(item => item !== null);

        itemsArray.sort((a, b) => (b.median ?? 0) - (a.median ?? 0));

        if (itemsArray.length === 0) {
            if (chartInstance) { chartInstance.destroy(); chartInstance = null; }
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            ctx.fillStyle = '#aaa'; ctx.font = '16px sans-serif'; ctx.textAlign = 'center';
            ctx.fillText('No data to display', canvas.width / 2, canvas.height / 2);
            return;
        }

        const hasNegativeOrZero = itemsArray.some(item =>
            item && ( // Add safety check for item
                (item.min <= 0 && isFinite(item.min)) ||
                (item.avg <= 0) ||
                (item.median <= 0) ||
                (item.ci_lower <= 0) ||
                (item.trueValue <= 0)
            )
        );

        const newYScaleType = (hasNegativeOrZero) ? 'linear' : 'logarithmic';

        // --- Calculate focused Y-axis scale ---
        let newScaleMin = undefined;
        let newScaleMax = undefined;

        const allPositiveData = itemsArray.flatMap(item =>
            item ? [item.min, item.avg, item.median, item.ci_lower, item.ci_upper, item.max, item.trueValue] : []
        ).filter(v => v > 0 && isFinite(v));

        if (allPositiveData.length > 0) {
            const trueDataMin = Math.min(...allPositiveData);
            const trueDataMax = Math.max(...allPositiveData);

            if (newYScaleType === 'logarithmic') {
                newScaleMin = Math.pow(10, Math.floor(Math.log10(trueDataMin)));
            } else {
                const coreData = itemsArray.flatMap(item => item ? [item.median, item.ci_lower, item.ci_upper, item.trueValue] : []).filter(v => isFinite(v));
                if (coreData.length > 0) {
                    let min = Math.min(...coreData);
                    let max = Math.max(...coreData);
                    const overallMin = Math.min(...allPositiveData);
                    const overallMax = Math.max(...allPositiveData);
                    if (overallMin < min) min = overallMin;
                    if (overallMax > max) max = overallMax;

                    const padding = (max - min) * 0.1 || 10;
                    newScaleMin = min - padding;
                    newScaleMax = max + padding;
                }
            }
        }
        // --- END NEW SCALE CALCULATION ---

        // Shortening logic for x-axis labels
        const chartLabelCallback = function(value) {
            const fullLabel = this.getLabelForValue(value);
            let targetName = fullLabel;
            const arrowIndex = fullLabel.indexOf('->');

            if (arrowIndex > -1) {
                const afterArrow = fullLabel.substring(arrowIndex + 2).trim();
                if (/^(&|Enh|\d|\s|\+)+$/.test(afterArrow) && afterArrow.length < 10) {
                    targetName = fullLabel.substring(0, arrowIndex).trim();
                } else {
                    targetName = afterArrow;
                }
            }

            const junkWords = ['&', 'of', 'the', 'a', 'an'];
            const cleanedName = targetName.replace(/:/g, ' ').replace(/&/g, '').replace(/Enh/g, '').replace(/\d+/g, '').replace(/\+/g, '').replace(/ +/g, ' ').trim();
            const parts = cleanedName.split(' ').filter(p => p && !junkWords.includes(p.toLowerCase()));

            const firstWord = parts[0] ? parts[0].substring(0, 5) : '';
            const secondWord = parts[1] ? parts[1].substring(0, 5) : '';

            const label = secondWord ? `${firstWord} ${secondWord}` : firstWord;
            return label || fullLabel.substring(0,10); // Fallback
        };

        if (!chartInstance) {
            const datasets = [
                { label: '95% CI', type: 'bar', data: itemsArray.map(item => ({ x: item.name, y: (item && item.n > 1 && (newYScaleType === 'linear' || item.ci_lower > 0)) ? [item.ci_lower, item.ci_upper] : null })), backgroundColor: 'rgba(100, 100, 100, 0.5)', borderColor: 'rgba(150, 150, 150, 0.7)', borderWidth: 1, barPercentage: 0.1, categoryPercentage: 0.5, order: 1, hidden: isIsolateTrueValue },
                { label: 'Min', data: itemsArray.map(item => ({ x: item.name, y: (item && (newYScaleType === 'linear' || item.min > 0)) ? item.min : null })), type: 'scatter', backgroundColor: 'rgba(34, 197, 94, 1)', borderColor: 'rgba(34, 197, 94, 1)', borderWidth: 3, pointRadius: 6, pointStyle: 'line', pointHoverRadius: 8, showLine: false, order: 2, hidden: isIsolateTrueValue },
                { label: 'Max', data: itemsArray.map(item => ({ x: item.name, y: (item && (newYScaleType === 'linear' || item.max > 0)) ? item.max : null })), type: 'scatter', backgroundColor: 'rgba(239, 68, 68, 1)', borderColor: 'rgba(239, 68, 68, 1)', borderWidth: 3, pointRadius: 6, pointStyle: 'line', pointHoverRadius: 8, showLine: false, order: 3, hidden: isIsolateTrueValue },
                { label: 'Average', data: itemsArray.map(item => ({ x: item.name, y: (item && (newYScaleType === 'linear' || item.avg > 0)) ? item.avg : null })), type: 'scatter', backgroundColor: 'rgba(255, 206, 86, 1)', borderColor: 'rgba(255, 206, 86, 1)', borderWidth: 2, pointRadius: 3, pointStyle: 'rectRot', pointHoverRadius: 5, showLine: false, order: 4, hidden: isIsolateTrueValue },
                { label: 'Median', data: itemsArray.map(item => ({ x: item.name, y: (item && (newYScaleType === 'linear' || item.median > 0)) ? item.median : null })), type: 'scatter', backgroundColor: 'rgba(153, 102, 255, 1)', borderColor: 'rgba(153, 102, 255, 1)', borderWidth: 2, pointRadius: 3, pointStyle: 'triangle', pointHoverRadius: 5, showLine: false, spanGaps: true, order: 5, hidden: isIsolateTrueValue },
                { label: 'True Value', data: itemsArray.map(item => ({ x: item.name, y: (item && item.trueValue !== null && (newYScaleType === 'linear' || item.trueValue > 0)) ? item.trueValue : null })), type: 'scatter', backgroundColor: 'rgba(54, 162, 235, 1)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 2, pointRadius: 5, pointStyle: 'star', pointHoverRadius: 7, showLine: true, spanGaps: true, order: 6 }
            ];
            chartInstance = new Chart(ctx, {
                type: 'bar',
                data: { labels: itemsArray.map(item => item.name), datasets },
                options: {
                    responsive: true, maintainAspectRatio: false, animation: { duration: 750, easing: 'easeInOutQuart' },
                    interaction: { mode: 'index', intersect: false },
                    plugins: {
                        title: { display: true, text: `${config.chartLabel} - Item Statistics`, color: '#eee', font: { size: 16 } },
                        legend: { display: true, position: 'top', labels: { color: '#eee', font: { size: 12 }, usePointStyle: true } },
                        tooltip: {
                            callbacks: {
                                label: function(context) {
                                    const label = context.dataset.label || '';
                                    let valueLabel = '';
                                    if (context.dataset.type === 'bar') {
                                        const value = context.parsed._custom;
                                        if (value) {
                                            valueLabel = `[${formatValue(value.min, currentMetric, true)}, ${formatValue(value.max, currentMetric, true)}]`;
                                        } else {
                                            valueLabel = 'N/A';
                                        }
                                    } else {
                                        valueLabel = formatValue(context.parsed.y, currentMetric, true);
                                    }
                                    return `${label}: ${valueLabel}`;
                                }
                            }
                        }
                    },
                    scales: {
                        x: { type: 'category', labels: itemsArray.map(item => item.name), offset: true, ticks: { color: '#eee', maxRotation: 45, minRotation: 45, font: { size: 10 }, callback: chartLabelCallback }, grid: { color: 'rgba(255, 255, 255, 0.1)', offset: true } },
                        y: {
                            type: newYScaleType,
                            min: newScaleMin,
                            max: newScaleMax,
                            ticks: { color: '#eee', callback: val => formatValue(val, currentMetric, true) },
                            grid: { color: 'rgba(255, 255, 255, 0.1)' },
                            title: { display: true, text: `${config.chartLabel} Amount (${newYScaleType} Scale)`, color: '#eee' }
                        }
                    }
                }
            });
        } else {
            // Chart exists, update it
            const itemNames = itemsArray.map(item => item.name);
            chartInstance.data.labels = itemNames;
            chartInstance.options.scales.x.labels = itemNames;
            chartInstance.options.plugins.title.text = `${config.chartLabel} - Item Statistics`;
            chartInstance.options.scales.y.type = newYScaleType;
            chartInstance.options.scales.y.title.text = `${config.chartLabel} Amount (${newYScaleType} Scale)`;
            chartInstance.options.scales.x.ticks.callback = chartLabelCallback;
            chartInstance.options.scales.y.min = newScaleMin;
            chartInstance.options.scales.y.max = newScaleMax;

            chartInstance.data.datasets[0].data = itemsArray.map(item => ({ x: item.name, y: (item && item.n > 1 && (newYScaleType === 'linear' || item.ci_lower > 0)) ? [item.ci_lower, item.ci_upper] : null }));
            chartInstance.data.datasets[1].data = itemsArray.map(item => ({ x: item.name, y: (item && (newYScaleType === 'linear' || item.min > 0)) ? item.min : null }));
            chartInstance.data.datasets[2].data = itemsArray.map(item => ({ x: item.name, y: (item && (newYScaleType === 'linear' || item.max > 0)) ? item.max : null }));
            chartInstance.data.datasets[3].data = itemsArray.map(item => ({ x: item.name, y: (item && (newYScaleType === 'linear' || item.avg > 0)) ? item.avg : null }));

            chartInstance.data.datasets[4].data = itemsArray.map(item => ({ x: item.name, y: (item && (newYScaleType === 'linear' || item.median > 0)) ? item.median : null }));
            chartInstance.data.datasets[4].showLine = false;

            if (!chartInstance.data.datasets[5]) {
                chartInstance.data.datasets[5] = { label: 'True Value', data: [], type: 'scatter', backgroundColor: 'rgba(54, 162, 235, 1)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 2, pointRadius: 5, pointStyle: 'star', pointHoverRadius: 7, showLine: true, spanGaps: true, order: 6 };
            }
            chartInstance.data.datasets[5].data = itemsArray.map(item => ({ x: item.name, y: (item && item.trueValue !== null && (newYScaleType === 'linear' || item.trueValue > 0)) ? item.trueValue : null }));
            chartInstance.data.datasets[5].showLine = true;
            chartInstance.data.datasets[5].spanGaps = true;

            chartInstance.data.datasets[0].hidden = isIsolateTrueValue;
            chartInstance.data.datasets[1].hidden = isIsolateTrueValue;
            chartInstance.data.datasets[2].hidden = isIsolateTrueValue;
            chartInstance.data.datasets[3].hidden = isIsolateTrueValue;
            chartInstance.data.datasets[4].hidden = isIsolateTrueValue;
            chartInstance.data.datasets[5].hidden = false;

            chartInstance.update('active');
        }
    }

    function exportToCSV() { try { console.log('JIGS Stats: Starting CSV export...'); let csv = '';

        if (currentMetric === 'trueValueSummary') {
            csv += `True Value Summary\n`;
            let header = 'Item Name,Count,N/A Count';
            for (const key in METRIC_CONFIG) { header += `,${METRIC_CONFIG[key].label}`; } // Use full label
            csv += header + '\n';

            let itemsArray = Array.from(itemAggregation.entries()).map(([itemName, itemMetrics]) => {
                const itemData = { name: itemName };
                const firstMetricKey = Object.keys(METRIC_CONFIG)[0];
                const defaultMetricData = itemMetrics.get('gPerProfit') || itemMetrics.get(firstMetricKey);
                if (!defaultMetricData) return null;
                itemData.count = defaultMetricData.count;
                itemData.naCount = defaultMetricData.naCount;

                for (const metricKey in METRIC_CONFIG) {
                    const config = METRIC_CONFIG[metricKey];
                    const metricData = itemMetrics.get(metricKey);
                    if (!metricData) {
                        itemData[metricKey + '_text'] = 'N/A';
                        continue;
                    }
                    const useZerosForStats = config.allowZero;
                    const valuesToProcess = isWinsorized ? winsorizeData(metricData.values, 0.05) : metricData.values;
                    const relevantValues = useZerosForStats ? valuesToProcess.filter(isFinite) : valuesToProcess.filter(v => v !== 0 && isFinite(v));
                    const total = valuesToProcess.reduce((sum, val) => sum + (isFinite(val) ? val : 0), 0);
                    const avg = metricData.count > 0 ? total / metricData.count : 0;
                    const median = calculateMedian(valuesToProcess, useZerosForStats);
                    const stats = calculateStatistics(relevantValues, useZerosForStats);
                    const ci_lower = (stats.n > 1) ? stats.mean - (1.96 * stats.se) : 0;
                    const ci_upper = (stats.n > 1) ? stats.mean + (1.96 * stats.se) : 0;

                    const hasValidCi = stats.n > 1;
                    const calculatedTv = (avg + median) / 2;
                    if (hasValidCi && calculatedTv >= ci_lower && calculatedTv <= ci_upper) {
                        itemData[metricKey + '_text'] = calculatedTv.toString();
                    } else {
                        itemData[metricKey + '_text'] = 'Not Enough Data';
                    }
                }
                return itemData;
            }).filter(item => item !== null);

            itemsArray.sort((a, b) => a.name.localeCompare(b.name));

            for (const item of itemsArray) {
                let row = `"${item.name}",${item.count},${item.naCount}`;
                for (const metricKey in METRIC_CONFIG) {
                    row += `,"${item[metricKey + '_text']}"`;
                }
                csv += row + '\n';
            }

        } else {
            const config = METRIC_CONFIG[currentMetric];
            const metricLabel = config.label.replace('G/0.01% ', '');
            csv += `Aggregated Results (Metric: ${config.label})\n`;
            csv += `Item Name,Count,N/A Count,True Value,Total ${metricLabel},Average (${metricLabel}),Median (${metricLabel}),Std Dev (${metricLabel}),95% CI Lower,95% CI Upper,T-Stat,Min ${metricLabel},Max ${metricLabel},Min/Max %Var,Avg Under,Avg Over,Avg U/O %Var\n`;
            const itemsArray = Array.from(itemAggregation.entries()).map(([itemName, itemMetrics]) => { const metricData = itemMetrics.get(currentMetric); if (!metricData) return null; const useZerosForStats = config.allowZero; const valuesToProcess = isWinsorized ? winsorizeData(metricData.values, 0.05) : metricData.values; const relevantValues = useZerosForStats ? valuesToProcess.filter(isFinite) : valuesToProcess.filter(v => v !== 0 && isFinite(v)); const total = valuesToProcess.reduce((sum, val) => sum + (isFinite(val) ? val : 0), 0); const avg = metricData.count > 0 ? total / metricData.count : 0; const median = calculateMedian(valuesToProcess, useZerosForStats); const stats = calculateStatistics(relevantValues, useZerosForStats); const min = relevantValues.length > 0 ? Math.min(...relevantValues) : 0; const max = relevantValues.length > 0 ? Math.max(...relevantValues) : 0; const valuesUnder = relevantValues.filter(v => v < stats.mean); const valuesOver = relevantValues.filter(v => v > stats.mean); const avgUnder = valuesUnder.length > 0 ? valuesUnder.reduce((sum, val) => sum + val, 0) / valuesUnder.length : 0; const avgOver = valuesOver.length > 0 ? valuesOver.reduce((sum, val) => sum + val, 0) / valuesOver.length : 0; const ci_lower = (stats.n > 1) ? stats.mean - (1.96 * stats.se) : 0; const ci_upper = (stats.n > 1) ? stats.mean + (1.96 * stats.se) : 0; const tStat = (stats.n > 1 && stats.se > 0) ? stats.mean / stats.se : 0;

                const hasValidCi = stats.n > 1;
                let trueValueText;
                const calculatedTv = (avg + median) / 2;
                if (hasValidCi && calculatedTv >= ci_lower && calculatedTv <= ci_upper) {
                    trueValueText = calculatedTv.toString();
                } else {
                    trueValueText = 'Not Enough Data';
                }

                return {
                    name: itemName,
                    count: metricData.count,
                    naCount: metricData.naCount,
                    trueValueText,
                    total,
                    avg,
                    median,
                    stddev: stats.stddev,
                    ci_lower,
                    ci_upper,
                    tStat,
                    min,
                    max,
                    avgUnder,
                    avgOver,
                    minMaxVariance: calculateVariancePct(avg, min, max),
                    avgUOVariance: calculateAvgUOVariancePct(avg, avgUnder, avgOver)
                };
            }).filter(item => item !== null); itemsArray.sort((a, b) => a.name.localeCompare(b.name));
            for (const item of itemsArray) {
                csv += `"${item.name}",${item.count},${item.naCount},"${item.trueValueText}",${item.total},${item.avg},${item.median},${item.stddev},${item.ci_lower},${item.ci_upper},${item.tStat},${item.min},${item.max},"${item.minMaxVariance}",${item.avgUnder},${item.avgOver},"${item.avgUOVariance}"\n`;
            }
            csv += `\nLine-by-Line Updates (Metric: ${config.label})\n`;
            csv += `Update #,Timestamp,Item Name,Count,N/A Count,True Value,Total ${metricLabel},Average (${metricLabel}),Median (${metricLabel}),Std Dev (${metricLabel}),95% CI Lower,95% CI Upper,T-Stat,Min ${metricLabel},Max ${metricLabel},Min/Max %Var,Avg Under,Avg Over,Avg U/O %Var\n`;
            for (const line of lineByLineData) {
                const stats = line.stats[currentMetric];

                let trueValueText;
                if (stats.trueValue !== null) {
                    trueValueText = stats.trueValue.toString();
                } else {
                    trueValueText = 'Not Enough Data';
                }

                if (stats) {
                    csv += `${line.id},${line.timestamp},"${line.itemName}",${stats.count},${stats.naCount},"${trueValueText}",${stats.total},${stats.avg},${stats.median},${stats.stddev},${stats.ci_lower},${stats.ci_upper},${stats.tStat},${stats.min},${stats.max},"${stats.minMaxVariance}",${stats.avgUnder},${stats.avgOver},"${stats.avgUOVariance}"\n`;
                }
            }
        }

        const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
        const link = document.createElement('a');
        const url = URL.createObjectURL(blob);
        link.setAttribute('href', url);
        const filename = `jigs-stats-export-${currentMetric}-${new Date().toISOString().slice(0,10)}.csv`;
        link.setAttribute('download', filename);
        link.style.visibility = 'hidden';
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        console.log(`JIGS Stats: CSV export complete - ${filename}`);
    } catch (error) {
        console.error('JIGS Stats: Error exporting CSV:', error);
        alert('Error exporting CSV. Check console for details.');
    }
}

    // --- OBSERVE JIGS RESULTS ---
    function observeJigsResults() {
        const jigsResultsTable = document.querySelector('#batch-results-table tbody');
        if (!jigsResultsTable) {
            console.log("JIGS Stats: JIGS results table body not found yet, will retry...");
            setTimeout(observeJigsResults, 1000);
            return;
        }

        console.log("JIGS Stats: Observing JIGS results table");
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeName === 'TR' && node.dataset.upgrade) { // Check if it's a JIGS data row
                            const upgradeText = node.dataset.upgrade.trim();
                            const itemName = extractItemName(upgradeText);
                            updateAggregation(itemName, node);
                        }
                    });
                }
            });
        });
        observer.observe(jigsResultsTable, { childList: true, subtree: false });

        const clearButton = document.getElementById('clear-results-button');
        if (clearButton) {
            clearButton.addEventListener('click', () => {
                console.log("JIGS Stats: Clearing data");
                setTimeout(clearRiggerData, 100);
            });
        }

        const thead = document.querySelector('#rigger-results-table thead');
        if (thead) {
            thead.addEventListener('click', event => {
                const headerCell = event.target.closest('th');
                if (!headerCell) return;
                const sortKey = headerCell.dataset.sortKey;
                if (!sortKey) return;

                if (currentSortKey === sortKey) {
                    currentSortDirection *= -1;
                } else {
                    currentSortKey = sortKey;
                    currentSortDirection = 1;
                }

                thead.querySelectorAll('th').forEach(th => th.classList.remove('sorted-asc', 'sorted-desc'));
                headerCell.classList.add(currentSortDirection === 1 ? 'sorted-asc' : 'sorted-desc');

                updateRiggerTable();
            });
        }
    }

    // --- INITIALIZATION WRAPPER ---
    function initializeWhenReady() {
        if (!document.body || !document.getElementById('batch-results-table')) { // Also wait for JIGS table
            console.log("JIGS Stats: Waiting for document body and JIGS table...");
            setTimeout(initializeWhenReady, 200);
            return;
        }

        // --- CREATE PANEL ---
        const riggerPanel = document.createElement('div');
        riggerPanel.id = 'jig-rigger-panel';

        let metricSelectorsHTML = '<div id="jr-metric-selector">';
        for (const key in METRIC_CONFIG) { const checked = key === currentMetric ? 'checked' : ''; metricSelectorsHTML += `<label><input type="radio" name="jr-metric" value="${key}" ${checked}> ${METRIC_CONFIG[key].label}</label>`; }
        const tvChecked = currentMetric === 'trueValueSummary' ? 'checked' : '';
        metricSelectorsHTML += `<label><input type="radio" name="jr-metric" value="trueValueSummary" ${tvChecked}> <strong>True Value</strong></label>`;
        metricSelectorsHTML += '</div>';

        let winsorizeHTML = `<label id="jr-winsorize-label" title="Winsorize data (clip outliers) at 5% and 95% percentile before calculating stats."><input type="checkbox" id="jr-winsorize-checkbox"> Winsorize</label>`;
        let isolateTvHTML = `<label id="jr-isolate-tv-label" title="Isolate True Value on chart"><input type="checkbox" id="jr-isolate-tv-checkbox"> Isolate TV</label>`;

        riggerPanel.innerHTML = `
            <div id="jig-rigger-header">
                <span>JIGS Stats</span>
                ${metricSelectorsHTML}
                <div class="jr-header-controls">
                    ${winsorizeHTML}
                    ${isolateTvHTML}
                    <button id="jr_toggle-chart-button" title="Toggle Chart">📊 Chart</button>
                    <button id="jr_export-csv-button" title="Export to CSV">💾 Export CSV</button>
                    <button id="rigger-toggle">-</button>
                </div>
            </div>
            <div id="jig-rigger-content">
                <div id="jr_chart-container" style="display: none;">
                    <canvas id="jr_chart-canvas"></canvas>
                </div>
                <div id="aggregated-section">
                    <div class="jr-section-header" id="aggregated-header">
                        <span>Aggregated Results</span>
                        <div id="jigs-rank-ledger">
                          <span>Rank:</span>
                          <span class="rank-1-box">1</span>
                          <span class="rank-2-box">2</span>
                          <span class="rank-3-box">3</span>
                          <span class="rank-4-box">4</span>
                          <span class="rank-5-box">5</span>
                        </div>
                        <button class="jr-section-toggle" id="aggregated-toggle">-</button>
                    </div>
                    <div id="rigger-results-container">
                        <table id="rigger-results-table">
                            <thead>
                                <tr>
                                    </tr>
                            </thead>
                            <tbody></tbody>
                        </table>
                    </div>
                </div>
                <div id="line-by-line-section">
                    <div class="jr-section-header" id="line-by-line-header">
                        <span>Line-by-Line Updates</span>
                        <button class="jr-section-toggle" id="line-by-line-toggle">-</button>
                    </div>
                    <div id="line-by-line-content">
                        <table id="line-by-line-table">
                            <thead>
                                <tr>
                                    </tr>
                            </thead>
                            <tbody></tbody>
                        </table>
                    </div>
                </div>
            </div>
            <div class="jig-rigger-resizer"></div>
        `;

        // --- APPEND PANEL TO BODY ---
        document.body.appendChild(riggerPanel);

        // --- STYLES ---
        GM_addStyle(`
            #jig-rigger-panel { position: fixed; top: 10px; left: 10px; width: 900px; height: 600px; background-color: #2c2c2c; border: 1px solid #444; border-radius: 5px; color: #eee; z-index: 9996; font-family: sans-serif; display: flex; flex-direction: column; overflow: hidden; }
            #jig-rigger-header { background-color: #333; padding: 8px; cursor: move; display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; border-bottom: 1px solid #444; flex-shrink: 0;}
            #jig-rigger-header span { font-weight: bold; }
            .jr-header-controls { display: flex; align-items: center; gap: 10px; }
            #export-csv-button, #jr_export-csv-button, #jr_toggle-chart-button, #rigger-toggle { background: #555; border: 1px solid #777; color: white; border-radius: 3px; cursor: pointer; padding: 4px 8px; }
            #export-csv-button:hover, #jr_export-csv-button:hover, #jr_toggle-chart-button:hover, #rigger-toggle:hover { background: #666; }
            #jr-metric-selector { display: flex; justify-content: center; flex-wrap: wrap; gap: 10px; background-color: #444; padding: 4px 8px; border-radius: 4px; }
            #jr-metric-selector label { cursor: pointer; color: #ccc; white-space: nowrap; font-size: 0.85em; }
            #jr-metric-selector input[type="radio"] { margin-right: 4px; vertical-align: middle; }
            #jr-metric-selector label:has(input:checked) { color: #fff; font-weight: bold; }
            #jr-winsorize-label, #jr-isolate-tv-label { font-size: 0.9em; color: #ccc; cursor: pointer; white-space: nowrap; }
            #jr-winsorize-label:has(input:checked), #jr-isolate-tv-label:has(input:checked) { color: #fff; font-weight: bold; }
            #jr-winsorize-label input, #jr-isolate-tv-label input { vertical-align: middle; margin-right: 4px; }
            #jr_chart-container { width: 100%; height: 400px; padding: 10px; background-color: #2a2a2a; border: 1px solid #444; border-radius: 3px; margin-bottom: 10px; flex-shrink: 0; }
            #jr_chart-canvas { width: 100% !important; height: 100% !important; }
            #jig-rigger-content { padding: 10px; display: flex; flex-direction: column; flex-grow: 1; overflow: hidden; gap: 10px; }
            #aggregated-section, #line-by-line-section { border: 1px solid #444; border-radius: 3px; display: flex; flex-direction: column; overflow: hidden; }
            .jr-section-header { background-color: #333; padding: 6px 8px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #444; flex-shrink: 0;}
            .jr-section-header span { font-weight: bold; font-size: 0.95em; }
            .jr-section-toggle { background: #555; border: 1px solid #777; color: white; border-radius: 3px; cursor: pointer; padding: 2px 6px; font-size: 0.9em; }
            #aggregated-section.collapsed #rigger-results-container, #line-by-line-section.collapsed #line-by-line-content { display: none; }
            #aggregated-section.collapsed, #line-by-line-section.collapsed { flex-grow: 0; min-height: 0; height: auto; }
            #rigger-results-container, #line-by-line-content { overflow-y: auto; flex-grow: 1; padding: 5px; min-height: 50px; }
            #rigger-results-container { max-height: 250px; }
            #line-by-line-content { max-height: 150px; }
            #rigger-results-table, #line-by-line-table { width: 100%; border-collapse: collapse; }
            #rigger-results-table th, #rigger-results-table td, #line-by-line-table th, #line-by-line-table td { border: 1px solid #444; padding: 5px; text-align: left; font-size: 0.8em; white-space: nowrap; }
            #rigger-results-table th, #line-by-line-table th { background-color: #333; position: sticky; top: 0; z-index: 1; cursor: pointer; }
            #rigger-results-table th[title], #line-by-line-table th[title] { cursor: help; text-decoration: underline dotted; text-decoration-thickness: 1px; }
            #rigger-results-table th:hover, #line-by-line-table th:hover { background-color: #444; }

            #rigger-results-table.jigs-summary-table td:nth-child(n+4),
            #rigger-results-table.jigs-metric-table td:nth-child(n+5),
            #line-by-line-table td:nth-child(n+5) { text-align: right; }

            .jigs-tv-best { background-color: rgba(34, 139, 34, 0.4); font-weight: bold; color: #fff !important; }
            .jigs-tv-worst { background-color: rgba(220, 20, 60, 0.4); color: #fff !important; }

            /* --- NEW: Rank Ledger & Row Styles --- */
            #jigs-rank-ledger { display: none; align-items: center; gap: 5px; font-size: 0.9em; margin-left: auto; margin-right: 10px; }
            #jigs-rank-ledger span { vertical-align: middle; }
            #jigs-rank-ledger [class*="-box"] { padding: 2px 6px; border-radius: 3px; color: #000; font-weight: bold; }
            .rank-1-box { background-color: #228B22; color: #fff; } /* ForestGreen */
            .rank-2-box { background-color: #4682B4; color: #fff; } /* SteelBlue */
            .rank-3-box { background-color: #DAA520; } /* GoldenRod */
            .rank-4-box { background-color: #CD853F; } /* Peru */
            .rank-5-box { background-color: #808080; color: #fff; } /* Gray */

            #rigger-results-table.jigs-summary-table td.jigs-rank-1 { background-color: #228B22; color: #fff; font-weight: bold; }
            #rigger-results-table.jigs-summary-table td.jigs-rank-2 { background-color: #4682B4; color: #fff; font-weight: bold; }
            #rigger-results-table.jigs-summary-table td.jigs-rank-3 { background-color: #DAA520; color: #000; font-weight: bold; }
            #rigger-results-table.jigs-summary-table td.jigs-rank-4 { background-color: #CD853F; color: #fff; font-weight: bold; }
            #rigger-results-table.jigs-summary-table td.jigs-rank-5 { background-color: #808080; color: #fff; font-weight: bold; }
            /* --- END NEW --- */

            .sorted-asc::after { content: ' ▲'; }
            .sorted-desc::after { content: ' ▼'; }
            #line-by-line-table tbody tr:nth-child(odd) { background-color: #2a2a2a; }
            .jig-rigger-resizer { position: absolute; width: 12px; height: 12px; right: 0; bottom: 0; cursor: se-resize; }
            #jig-rigger-panel.jig-rigger-minimized { position: fixed !important; top: 150px !important; right: 10px !important; left: auto !important; bottom: auto !important; width: auto !important; height: auto !important; z-index: 9997; }
            #jig-rigger-panel.jig-rigger-minimized #jig-rigger-content, #jig-rigger-panel.jig-rigger-minimized .jig-rigger-resizer, #jig-rigger-panel.jig-rigger-minimized #jr-metric-selector { display: none; }
            #jig-rigger-panel.jig-rigger-minimized #jig-rigger-header { cursor: pointer; }
        `);

        // --- INITIALIZE (Listeners, Initial State) ---
        setTimeout(function() {
            const riggerPanelElement = document.getElementById('jig-rigger-panel');
            const riggerHeaderElement = document.getElementById('jig-rigger-header');
            const riggerResizerElement = riggerPanelElement ? riggerPanelElement.querySelector('.jig-rigger-resizer') : null;

            if (riggerPanelElement && riggerHeaderElement && riggerResizerElement) {
                console.log("JIGS Stats: Panel elements found. Initializing...");
                makeDraggable(riggerPanelElement, riggerHeaderElement);
                makeResizable(riggerPanelElement, riggerResizerElement);

                try {
                    document.getElementById('rigger-toggle').addEventListener('click', function() { const isMinimized = riggerPanelElement.classList.contains('jig-rigger-minimized'); if (!isMinimized) { originalPanelPosition.top = riggerPanelElement.style.top || '10px'; originalPanelPosition.left = riggerPanelElement.style.left || '10px'; } riggerPanelElement.classList.toggle('jig-rigger-minimized'); this.textContent = riggerPanelElement.classList.contains('jig-rigger-minimized') ? '+' : '-'; if (!riggerPanelElement.classList.contains('jig-rigger-minimized')) { riggerPanelElement.style.top = originalPanelPosition.top; riggerPanelElement.style.left = originalPanelPosition.left; riggerPanelElement.style.right = 'auto'; riggerPanelElement.style.bottom = 'auto'; const savedPositions = GM_getValue('jig_rigger_panel_position', {}); savedPositions.top = riggerPanelElement.style.top; savedPositions.left = riggerPanelElement.style.left; GM_setValue('jig_rigger_panel_position', savedPositions); } GM_setValue('jig_rigger_minimized', riggerPanelElement.classList.contains('jig-rigger-minimized')); });
                    document.getElementById('aggregated-toggle').addEventListener('click', function() { const section = document.getElementById('aggregated-section'); section.classList.toggle('collapsed'); this.textContent = section.classList.contains('collapsed') ? '+' : '-'; GM_setValue('jig_rigger_aggregated_collapsed', section.classList.contains('collapsed')); });
                    document.getElementById('line-by-line-toggle').addEventListener('click', function() { const section = document.getElementById('line-by-line-section'); section.classList.toggle('collapsed'); this.textContent = section.classList.contains('collapsed') ? '+' : '-'; GM_setValue('jig_rigger_line_by_line_collapsed', section.classList.contains('collapsed')); });
                    document.getElementById('jr_export-csv-button').addEventListener('click', exportToCSV);

                    document.getElementById('jr_toggle-chart-button').addEventListener('click', function() {
                        isChartVisible = !isChartVisible;
                        GM_setValue('jig_rigger_chart_visible', isChartVisible);
                        const chartContainer = document.getElementById('jr_chart-container');

                        if (isChartVisible && currentMetric !== 'trueValueSummary') {
                            chartContainer.style.display = 'block';
                            updateChart();
                        } else {
                            chartContainer.style.display = 'none';
                        }
                    });

                    document.querySelectorAll('#jr-metric-selector input[name="jr-metric"]').forEach(radio => {
                        radio.addEventListener('change', function() {
                            if (this.checked) {
                                currentMetric = this.value;
                                GM_setValue('jig_rigger_current_metric', currentMetric);
                                console.log("JIGS Stats: Metric changed to", currentMetric);

                                currentSortKey = 'name';
                                currentSortDirection = 1;

                                updateTableHeaders();
                                updateRiggerTable();
                                updateLineByLineTable(true);

                                const chartContainer = document.getElementById('jr_chart-container');
                                if (isChartVisible && currentMetric !== 'trueValueSummary') {
                                    chartContainer.style.display = 'block';
                                    updateChart();
                                } else {
                                    chartContainer.style.display = 'none';
                                }
                            }
                        });
                    });

                    // --- Winsorize Listener ---
                    document.getElementById('jr-winsorize-checkbox').addEventListener('change', function() {
                        isWinsorized = this.checked;
                        GM_setValue('jig_rigger_winsorized', isWinsorized);
                        console.log("JIGS Stats: Winsorize set to", isWinsorized);

                        updateRiggerTable();
                        updateLineByLineTable(true);
                        if (isChartVisible) updateChart();
                    });

                    // --- Isolate TV Listener ---
                    document.getElementById('jr-isolate-tv-checkbox').addEventListener('change', function() {
                        isIsolateTrueValue = this.checked;
                        GM_setValue('jig_rigger_isolate_tv', isIsolateTrueValue);
                        console.log("JIGS Stats: Isolate True Value set to", isIsolateTrueValue);

                        if (isChartVisible && chartInstance) {
                            chartInstance.data.datasets[0].hidden = isIsolateTrueValue;
                            chartInstance.data.datasets[1].hidden = isIsolateTrueValue;
                            chartInstance.data.datasets[2].hidden = isIsolateTrueValue;
                            chartInstance.data.datasets[3].hidden = isIsolateTrueValue;
                            chartInstance.data.datasets[4].hidden = isIsolateTrueValue;
                            chartInstance.update('active');
                        }
                    });

                } catch (error) { console.error("JIGS Stats: Error attaching event listener:", error); }

                applySavedPanelState();
                setTimeout(observeJigsResults, 100);

            } else {
                console.error("JIGS Stats: Could not find essential panel elements during initialization timeout!");
                if (!riggerPanelElement) console.error("Missing: #jig-rigger-panel");
                if (!riggerHeaderElement) console.error("Missing: #jig-rigger-header");
                if (riggerPanelElement && !riggerResizerElement) console.error("Missing: .jig-rigger-resizer inside panel");
            }
        }, 500); // Keep 500ms delay

    } // End of initializeWhenReady

    // --- Start Initialization Process ---
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeWhenReady);
    } else {
        // The DOMContentLoaded event has already fired
        initializeWhenReady();
    }

})(); // <-- End of main IIFE