JIGS Stats

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

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

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

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

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

您需要先安装一个扩展,例如 篡改猴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