Companion statistics panel for JIGS, with selectable metrics, advanced stats, charts with CI, collapsible sections
当前为
// ==UserScript== // @name JIGS Stats // @namespace http://tampermonkey.net/ // @version 1.0.1 // @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.1 Loaded"); // --- CONFIGURATION & STATE VARIABLES --- const METRIC_CONFIG = { 'dpsChange': { label: 'DPS Δ', datasetKey: 'dpsChange', format: 'number', allowZero: true, chartLabel: 'DPS Change' }, 'gPerDps': { label: 'G/0.01% DPS', datasetKey: 'costPerDps', format: 'gold', allowZero: false, chartLabel: 'Gold per 0.01% DPS' }, 'profitChange': { label: 'Profit Δ', datasetKey: 'profitChange', format: 'gold', allowZero: true, chartLabel: 'Profit Change' }, 'gPerProfit': { label: 'G/0.01% Profit', datasetKey: 'costPerProfit', format: 'gold', allowZero: false, chartLabel: 'Gold per 0.01% Profit' }, 'expChange': { label: 'Exp/Hr Δ', datasetKey: 'expChange', format: 'number', allowZero: true, chartLabel: 'Exp/Hr Change' }, 'gPerExpHr': { label: 'G/0.01% Exp/Hr', datasetKey: 'costPerExp', format: 'gold', allowZero: false, chartLabel: 'Gold per 0.01% Exp/Hr' }, 'ephChange': { label: 'EPH Δ', datasetKey: 'ephChange', format: 'number', allowZero: true, chartLabel: 'EPH Change' }, 'gPerEph': { label: 'G/0.01% EPH', 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 = '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 // ============================================= // === 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') 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 chartContainer = document.getElementById('jr_chart-container'); if(chartContainer) chartContainer.style.display = isChartVisible ? 'block' : 'none'; 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; } 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; lineEntryStats[metricKey] = { count: metricData.count, naCount: metricData.naCount, 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) }; } updateCounter++; const timestamp = new Date().toLocaleTimeString(); lineByLineData.push({ id: updateCounter, timestamp, itemName, stats: lineEntryStats }); updateRiggerTable(); updateLineByLineTable(); } function updateTableHeaders() { const aggTHead = document.querySelector('#rigger-results-table thead tr'); if (aggTHead) { aggTHead.querySelector('[data-sort-key="total"]').textContent = 'Total'; aggTHead.querySelector('[data-sort-key="avg"]').textContent = 'Avg'; aggTHead.querySelector('[data-sort-key="median"]').textContent = 'Median'; aggTHead.querySelector('[data-sort-key="stddev"]').textContent = 'Std Dev'; aggTHead.querySelector('[data-sort-key="ci"]').textContent = '95% CI'; aggTHead.querySelector('[data-sort-key="tStat"]').textContent = 'T-Stat'; aggTHead.querySelector('[data-sort-key="min"]').textContent = 'Min'; aggTHead.querySelector('[data-sort-key="max"]').textContent = 'Max'; aggTHead.querySelector('[data-sort-key="minMaxVariance"]').textContent = 'Min/Max %Var'; aggTHead.querySelector('[data-sort-key="avgUnder"]').textContent = 'Avg Under'; aggTHead.querySelector('[data-sort-key="avgOver"]').textContent = 'Avg Over'; aggTHead.querySelector('[data-sort-key="avgUOVariance"]').textContent = 'Avg U/O %Var'; } const lineTHead = document.querySelector('#line-by-line-table thead tr'); if (lineTHead) { lineTHead.children[5].textContent = 'Total'; lineTHead.children[6].textContent = 'Avg'; lineTHead.children[7].textContent = 'Median'; lineTHead.children[8].textContent = 'Std Dev'; lineTHead.children[9].textContent = '95% CI'; lineTHead.children[10].textContent = 'T-Stat'; lineTHead.children[11].textContent = 'Min'; lineTHead.children[12].textContent = 'Max'; lineTHead.children[13].textContent = 'Min/Max %Var'; lineTHead.children[14].textContent = 'Avg Under'; lineTHead.children[15].textContent = 'Avg Over'; lineTHead.children[16].textContent = 'Avg U/O %Var'; } } function updateRiggerTable() { 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; return { name: itemName, count: metricData.count, naCount: metricData.naCount, 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')) return 1 * currentSortDirection; if (valB === 'N/A' || valB.includes('N/A')) return -1 * currentSortDirection; return valA.localeCompare(valB) * 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>${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) { 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]; 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>${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() { 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; return { name: itemName, min, max, avg, median, ci_lower, ci_upper, n: stats.n }; }).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) ) ); 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] : [] ).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] : []).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 }, { 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 }, { 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 }, { 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 }, { 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: true, spanGaps: true, order: 5 } ]; 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; valueLabel = `[${formatValue(value.min, currentMetric, true)}, ${formatValue(value.max, currentMetric, true)}]`; } 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.update('active'); } } function exportToCSV() { try { console.log('JIGS Stats: Starting CSV export...'); let csv = ''; 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,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; return { name: itemName, count: metricData.count, naCount: metricData.naCount, 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.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,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]; if (stats) { csv += `${line.id},${line.timestamp},"${line.itemName}",${stats.count},${stats.naCount},${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); 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); // Pass the whole TR element (node) which has the data attributes 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>`; } 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>`; riggerPanel.innerHTML = ` <div id="jig-rigger-header"> <span>JIGS Stats</span> ${metricSelectorsHTML} <div class="jr-header-controls"> ${winsorizeHTML} <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> <button class="jr-section-toggle" id="aggregated-toggle">-</button> </div> <div id="rigger-results-container"> <table id="rigger-results-table"> <thead> <tr> <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="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> </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> <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="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> </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 { font-size: 0.9em; color: #ccc; cursor: pointer; white-space: nowrap; } #jr-winsorize-label:has(input:checked) { color: #fff; font-weight: bold; } #jr-winsorize-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 td:nth-child(n+2), #line-by-line-table td:nth-child(n+4) { text-align: right; } .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) --- // Use setTimeout to allow elements to render after innerHTML is set 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; const chartContainer = document.getElementById('jr_chart-container'); if (isChartVisible) { chartContainer.style.display = 'block'; updateChart(); } else { chartContainer.style.display = 'none'; } GM_setValue('jig_rigger_chart_visible', isChartVisible); }); 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); updateTableHeaders(); updateRiggerTable(); updateLineByLineTable(true); if (isChartVisible) updateChart(); } }); }); // --- 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); // Force a full recalculation and redraw of tables and chart updateRiggerTable(); updateLineByLineTable(true); // Redraw all rows if (isChartVisible) updateChart(); }); } catch (error) { console.error("JIGS Stats: Error attaching event listener:", error); } updateTableHeaders(); 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