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.1.0 // @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.1.7 Loaded"); // <-- Corrected version log // --- CONFIGURATION & STATE VARIABLES --- const METRIC_CONFIG = { 'dpsChange': { label: 'DPS Δ', isCostMetric: false, datasetKey: 'dpsChange', format: 'number', allowZero: true, chartLabel: 'DPS Change' }, 'profitChange': { label: 'Profit Δ', isCostMetric: false, datasetKey: 'profitChange', format: 'gold', allowZero: true, chartLabel: 'Profit Change' }, 'expChange': { label: 'Exp/Hr Δ', isCostMetric: false, datasetKey: 'expChange', format: 'number', allowZero: true, chartLabel: 'Exp/Hr Change' }, 'ephChange': { label: 'EPH Δ', isCostMetric: false, datasetKey: 'ephChange', format: 'number', allowZero: true, chartLabel: 'EPH Change' }, 'cost': { label: 'Cost', isCostMetric: true, datasetKey: 'cost', format: 'gold', allowZero: false, chartLabel: 'Cost', hidden: true }, 'timeToPurchase': { label: 'Time', isCostMetric: true, datasetKey: 'timeToPurchase', format: 'time', allowZero: true, chartLabel: 'Time', hidden: true } // *** FIX 1: Corrected datasetKey *** }; let currentMetric = GM_getValue('jig_rigger_current_metric', 'dpsChange'); if (currentMetric !== 'trueValueSummary' && (!METRIC_CONFIG[currentMetric] || METRIC_CONFIG[currentMetric].hidden)) { currentMetric = 'dpsChange'; } 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 // --- MODIFIED: Store all baseline stats --- let baselineProfit = 0; let baselineDps = 0; let baselineExp = 0; let baselineEph = 0; // ============================================= // === 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; } function parseValueFromDataset(rawValue) { if (rawValue === 'N/A' || rawValue === undefined || rawValue === null) return 0; if (rawValue === 'Free') return 0; if (rawValue === 'Never' || rawValue === 'Infinity') return Infinity; const numValue = parseFloat(rawValue); return isNaN(numValue) ? 0 : numValue; } function parseGoldValue(text) { if (!text) return 0; text = text.trim().toUpperCase().replace(/,/g, ''); const num = parseFloat(text); if (isNaN(num)) return 0; if (text.endsWith('K')) return num * 1000; if (text.endsWith('M')) return num * 1000000; if (text.endsWith('B')) return num * 1000000000; return num; } function parseNumberValue(text) { if (!text) return 0; text = text.trim().replace(/,/g, ''); const num = parseFloat(text); return isNaN(num) ? 0 : num; } // --- MODIFIED: Function to find all baseline stats from INPUT fields --- function getBaselineStats() { try { // Target the input fields directly const baseProfitInput = document.getElementById('baseline-profit-input'); const baseDpsInput = document.getElementById('baseline-dps-input'); const baseExpInput = document.getElementById('baseline-exp-input'); const baseEphInput = document.getElementById('baseline-eph-input'); // Read the .value property and parse it if (baseProfitInput) { baselineProfit = parseGoldValue(baseProfitInput.value); // Use parseGoldValue console.log('JIGS Stats: Baseline Profit captured from input:', baselineProfit); } else { console.warn('JIGS Stats: Could not find #baseline-profit-input.'); baselineProfit = 0;} if (baseDpsInput) { baselineDps = parseNumberValue(baseDpsInput.value); // Use parseNumberValue console.log('JIGS Stats: Baseline DPS captured from input:', baselineDps); } else { console.warn('JIGS Stats: Could not find #baseline-dps-input.'); baselineDps = 0;} if (baseExpInput) { // Exp might have commas, remove them before parsing baselineExp = parseNumberValue(baseExpInput.value.replace(/,/g, '')); // Use parseNumberValue console.log('JIGS Stats: Baseline Exp/Hr captured from input:', baselineExp); } else { console.warn('JIGS Stats: Could not find #baseline-exp-input.'); baselineExp = 0;} if (baseEphInput) { baselineEph = parseNumberValue(baseEphInput.value); // Use parseNumberValue console.log('JIGS Stats: Baseline EPH captured from input:', baselineEph); } else { console.warn('JIGS Stats: Could not find #baseline-eph-input.'); baselineEph = 0;} } catch (e) { console.error('JIGS Stats: Error capturing baseline stats from inputs.', e); // Default all baselines to 0 on error baselineProfit = 0; baselineDps = 0; baselineExp = 0; baselineEph = 0; } } function isNA(rawValue) { return rawValue === 'N/A' || rawValue === undefined || rawValue === null || rawValue === 'Never' || rawValue === 'Infinity'; } // --- Formatting Functions --- function formatValue(value, metricKey, allowZeroOverride = null) { const formatConfig = METRIC_CONFIG[metricKey]; if (!formatConfig) { if (metricKey === 'roi') return formatPercent(value); if (metricKey.startsWith('gPer')) return formatGoldValue(value, true); return "N/A"; } const allowZero = allowZeroOverride ?? formatConfig.allowZero; if (value === null || value === undefined || !isFinite(value)) return 'N/A'; if (value === 0 && !allowZero) return 'N/A'; switch(formatConfig.format) { case 'gold': return formatGoldValue(value, allowZero); case 'percent': return formatPercent(value); case 'time': return formatTime(value, metricKey); // <-- Pass metricKey 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'; if (value === Infinity) return '∞'; return `${value.toFixed(1)}%`; } // --- THIS IS THE CORRECTED v1.1.7 FUNCTION --- function formatTime(days, metricKey = null) { // --- MODIFIED: Handle 0 specifically for timeToPurchase --- if (days === 0 && metricKey === 'timeToPurchase') { return 'Never'; // If TV of Time is 0, it means underlying was likely Infinity } // --- END MODIFICATION --- if (!isFinite(days) || days === Infinity || days === null) { return 'Never'; } if (days <= 0) { return 'Free'; } // Keep "Free" for other potential time metrics or negative values 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; const sorted = [...finiteValues].sort((a, b) => a - b); const n = sorted.length; const numToClip = Math.ceil(percentile * n); if (numToClip === 0 || numToClip * 2 >= n) { return values; } const lowerLimit = sorted[numToClip]; const upperLimit = sorted[n - 1 - numToClip]; if (lowerLimit === upperLimit) return values; return values.map(val => { if (!isFinite(val)) return val; 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'); if (!aggTHeadTr || !lineByLineSection || !chartContainer || !isolateTvLabel || !lineTHeadTr || !aggSection || !aggContainer || !rankLedger) return; if (currentMetric === 'trueValueSummary') { let headers = '<th data-sort-key="name">Item Name</th><th data-sort-key="count">#</th>'; headers += '<th data-sort-key="cost">Upgrade Cost</th><th data-sort-key="timeToPurchase">Time</th>'; for (const key in METRIC_CONFIG) { if (METRIC_CONFIG[key].hidden) continue; headers += `<th data-sort-key="${key}" title="${METRIC_CONFIG[key].label}">${METRIC_CONFIG[key].label}</th>`; if (key.endsWith('Change')) { const newKey = `gPer${key.replace('Change', '')}`; let newLabel = `G/0.01% TV ${METRIC_CONFIG[key].label.replace('Δ', '')}`; headers += `<th data-sort-key="${newKey}" title="${newLabel}">${newLabel}</th>`; } } headers += '<th data-sort-key="roi">ROI (1yr)</th>'; aggTHeadTr.innerHTML = headers; aggTable.classList.add('jigs-summary-table'); aggTable.classList.remove('jigs-metric-table'); lineByLineSection.style.display = 'none'; chartContainer.style.display = 'none'; isolateTvLabel.style.display = 'none'; rankLedger.style.display = 'flex'; aggSection.style.flexGrow = '1'; aggContainer.style.maxHeight = 'none'; 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 { 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'); 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> `; lineByLineSection.style.display = 'flex'; isolateTvLabel.style.display = 'block'; rankLedger.style.display = 'none'; aggSection.style.flexGrow = '0'; aggContainer.style.maxHeight = '250px'; if (isChartVisible) { chartContainer.style.display = 'block'; } } } // --- THIS IS THE CORRECTED v1.1.7 FUNCTION --- function buildTrueValueSummaryTable() { // Read baselines just before calculating getBaselineStats(); const tbody = document.querySelector('#rigger-results-table tbody'); tbody.innerHTML = ''; // --- MODIFIED: Corrected key names --- const newCalculatedMetricKeys = ['gPerProfit', 'gPerDps', 'gPerExpHr', 'gPerEph', 'roi']; let itemsArray = Array.from(itemAggregation.entries()).map(([itemName, itemMetrics]) => { const itemData = { name: itemName }; const firstMetricKey = Object.keys(METRIC_CONFIG)[0]; const defaultMetricData = itemMetrics.get('profitChange') || itemMetrics.get(firstMetricKey); if (!defaultMetricData) return null; itemData.count = defaultMetricData.count; itemData.naCount = defaultMetricData.naCount; // Calculate True Values for raw metrics first for (const metricKey in METRIC_CONFIG) { const config = METRIC_CONFIG[metricKey]; const metricData = itemMetrics.get(metricKey); if (!metricData) { itemData[metricKey] = null; 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; if (metricKey === 'timeToPurchase') { trueValueText = formatTime(trueValue, metricKey); } else { trueValueText = formatValue(trueValue, metricKey, true); } } else { trueValueText = 'Not Enough Data'; } itemData[metricKey] = trueValue; itemData[metricKey + '_text'] = trueValueText; } // --- *** CALCULATE NEW METRICS *** --- const tvCost = itemData['cost']; // --- Helper function for G/% calculation --- const calculateGPer = (tvRawChange, baselineValue, cost) => { let tvPct = null; if (tvRawChange !== null && baselineValue > 0) { tvPct = (tvRawChange / baselineValue) * 100; } else if (tvRawChange !== null && tvRawChange > 0 && baselineValue <= 0) { tvPct = Infinity; // Positive gain from zero baseline } let gPerValue = null; if (cost !== null && tvPct !== null && isFinite(tvPct) && tvPct > 0) { gPerValue = (cost / (tvPct * 100)); } let gPerText; if (gPerValue !== null) { gPerText = formatValue(gPerValue, 'gPer', true); } else { if (cost === 0 && tvPct !== null && tvPct > 0) gPerText = "Free"; else if (cost !== null && cost > 0 && tvPct === Infinity) gPerText = "0"; else gPerText = "N/A"; } return { value: gPerValue, text: gPerText }; }; // --- 1. G/0.01% TV Profit --- const tvProfitRaw = itemData['profitChange']; const profitGPer = calculateGPer(tvProfitRaw, baselineProfit, tvCost); itemData['gPerProfit'] = profitGPer.value; itemData['gPerProfit_text'] = profitGPer.text; // --- 2. G/0.01% TV DPS --- const tvDpsRaw = itemData['dpsChange']; const dpsGPer = calculateGPer(tvDpsRaw, baselineDps, tvCost); itemData['gPerDps'] = dpsGPer.value; itemData['gPerDps_text'] = dpsGPer.text; // --- 3. G/0.01% TV Exp/Hr --- const tvExpRaw = itemData['expChange']; const expGPer = calculateGPer(tvExpRaw, baselineExp, tvCost); itemData['gPerExpHr'] = expGPer.value; itemData['gPerExpHr_text'] = expGPer.text; // --- 4. G/0.01% TV EPH --- const tvEphRaw = itemData['ephChange']; const ephGPer = calculateGPer(tvEphRaw, baselineEph, tvCost); itemData['gPerEph'] = ephGPer.value; itemData['gPerEph_text'] = ephGPer.text; // --- 5. ROI (1yr) --- let roi = null; if (tvCost !== null && tvProfitRaw !== null && tvProfitRaw > 0) { if (tvCost > 0) { const gainPerYear = tvProfitRaw * 24 * 365; roi = (gainPerYear / tvCost) * 100; } else if (tvCost === 0) { roi = Infinity; } } itemData['roi'] = roi; itemData['roi_text'] = formatValue(roi, 'roi'); // --- *** END CALCULATIONS *** --- return itemData; }).filter(item => item !== null); // Find column extremes for highlighting let columnExtremes = {}; for (const metricKey in METRIC_CONFIG) { if (METRIC_CONFIG[metricKey].hidden) continue; 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; columnExtremes[metricKey] = config.isCostMetric ? { best: minVal, worst: maxVal } : { best: maxVal, worst: minVal }; } for (const metricKey of newCalculatedMetricKeys) { 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; const isCost = (metricKey !== 'roi'); // roi is benefit, others are cost columnExtremes[metricKey] = isCost ? { best: minVal, worst: maxVal } : { best: maxVal, worst: minVal }; } // Calculate bestCount for each item itemsArray.forEach(item => { item.bestCount = 0; for (const metricKey in METRIC_CONFIG) { if (METRIC_CONFIG[metricKey].hidden) continue; const extremes = columnExtremes[metricKey]; if (extremes && item[metricKey] !== null && item[metricKey] === extremes.best) item.bestCount++; } for (const metricKey of newCalculatedMetricKeys) { const extremes = columnExtremes[metricKey]; if (metricKey === 'roi' && extremes && item[metricKey] === Infinity && extremes.best === Infinity){ item.bestCount++; } else if (extremes && item[metricKey] !== null && item[metricKey] === extremes.best) { item.bestCount++; } } }); const rankedItems = [...itemsArray] .filter(item => item.bestCount > 0) .sort((a, b) => b.bestCount - a.bestCount); const topItemNames = rankedItems.slice(0, 5).map(item => item.name); // Sort if (currentSortKey) { itemsArray.sort((a, b) => { let valA, valB, valA_text, valB_text; // --- MODIFIED: Use correct key casing --- if (METRIC_CONFIG[currentSortKey] || ['cost', 'timeToPurchase'].includes(currentSortKey)) { valA = a[currentSortKey]; valB = b[currentSortKey]; valA_text = a[currentSortKey + '_text']; 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_text === 'Never') return 1 * currentSortDirection; // 'Never' goes last if (valB_text === 'Never') return -1 * currentSortDirection; } else if (newCalculatedMetricKeys.includes(currentSortKey)) { // Handle calculated metrics valA = a[currentSortKey]; valB = b[currentSortKey]; valA_text = a[currentSortKey + '_text']; valB_text = b[currentSortKey + '_text']; if (valA_text === 'N/A' || valA_text === 'Never' || valA_text === 'Free') return 1 * currentSortDirection; // N/A, Never, Free go last if (valB_text === 'N/A' || valB_text === 'Never' || valB_text === 'Free') return -1 * currentSortDirection; if (currentSortKey === 'roi') { if (valA === Infinity && valB === Infinity) return 0; if (valA === Infinity) return -1 * currentSortDirection; // Infinity ROI is best if (valB === Infinity) return 1 * currentSortDirection; } } else if (currentSortKey === 'name' || currentSortKey === 'count') { valA = a[currentSortKey]; valB = b[currentSortKey]; if(currentSortKey === 'name') return valA.localeCompare(valB) * currentSortDirection; return (valA - valB) * currentSortDirection; } // General comparison for null/undefined/finite numbers if (valA === null || valA === undefined) return 1 * currentSortDirection; if (valB === null || valB === undefined) return -1 * currentSortDirection; return (valA - valB) * currentSortDirection; }); } else { itemsArray.sort((a, b) => a.name.localeCompare(b.name)); } // Render for (const item of itemsArray) { const row = tbody.insertRow(); 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>`; rowHTML += `<td class="jigs-tv-raw-data">${item['cost_text']}</td>`; rowHTML += `<td class="jigs-tv-raw-data">${item['timeToPurchase_text']}</td>`; for (const metricKey in METRIC_CONFIG) { if (METRIC_CONFIG[metricKey].hidden) continue; 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>`; if (metricKey.endsWith('Change')) { // --- MODIFIED: Use correct key casing --- let newKey = `gPer${metricKey.charAt(0).toUpperCase() + metricKey.slice(1).replace('Change', '')}`; // --- *** FIX 2: Manually correct expChange key *** --- if (metricKey === 'expChange') { newKey = 'gPerExpHr'; } // --- *** END FIX 2 *** --- const newExtremes = columnExtremes[newKey]; const newVal = item[newKey]; let newClassName = ''; if (newExtremes && newVal !== null && isFinite(newVal)) { if (newVal === newExtremes.best) newClassName = 'jigs-tv-best'; else if (newVal === newExtremes.worst) newClassName = 'jigs-tv-worst'; } else if (item[newKey + '_text'] === 'Free' && newExtremes && newExtremes.best === 0) { newClassName = 'jigs-tv-best'; } rowHTML += `<td class="${newClassName}">${item[newKey + '_text']}</td>`; } } let roiClassName = ''; const roiExtremes = columnExtremes['roi']; const roiVal = item['roi']; if (roiExtremes && roiVal !== null) { // Check includes Infinity if (roiVal === roiExtremes.best) roiClassName = 'jigs-tv-best'; else if (roiVal === roiExtremes.worst) roiClassName = 'jigs-tv-worst'; } rowHTML += `<td class="${roiClassName}">${item['roi_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]; if (!config) { console.error("JIGS Stats: Invalid currentMetric in updateRiggerTable:", currentMetric); return; } 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; if (!METRIC_CONFIG[currentMetric]) 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; } if (!METRIC_CONFIG[currentMetric]) { console.warn("JIGS Stats: addLineByLineRow called with invalid currentMetric:", currentMetric); 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]; if (!config) return; 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 && ( (item.min <= 0 && isFinite(item.min)) || (item.avg <= 0) || (item.median <= 0) || (item.ci_lower <= 0) || (item.trueValue <= 0) ) ); const newYScaleType = (hasNegativeOrZero) ? 'linear' : 'logarithmic'; let newScaleMin = undefined, 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); 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; } } } 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); }; 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 { 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,Upgrade Cost,Time'; for (const key in METRIC_CONFIG) { if (METRIC_CONFIG[key].hidden) continue; header += `,"${METRIC_CONFIG[key].label}"`; if (key.endsWith('Change')) { const newLabel = `G/0.01% TV ${METRIC_CONFIG[key].label.replace('Δ', '')}`; header += `,"${newLabel}"`; } } header += ',"ROI (1yr)"\n'; csv += header; let itemsArray = Array.from(itemAggregation.entries()).map(([itemName, itemMetrics]) => { const itemData = { name: itemName }; const firstMetricKey = Object.keys(METRIC_CONFIG)[0]; const defaultMetricData = itemMetrics.get('profitChange') || itemMetrics.get(firstMetricKey); if (!defaultMetricData) return null; itemData.count = defaultMetricData.count; for (const metricKey in METRIC_CONFIG) { const config = METRIC_CONFIG[metricKey]; const metricData = itemMetrics.get(metricKey); if (!metricData) { itemData[metricKey] = null; 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] = calculatedTv; // --- MODIFIED: Pass key to formatTime --- if (metricKey === 'timeToPurchase') { itemData[metricKey + '_text'] = formatTime(calculatedTv, metricKey); } else { itemData[metricKey + '_text'] = formatValue(calculatedTv, metricKey, true); } } else { itemData[metricKey] = null; itemData[metricKey + '_text'] = 'Not Enough Data'; } } // --- MODIFIED: Calculate all new metrics for export --- const tvCost = itemData['cost']; const tvProfitRaw = itemData['profitChange']; let tvProfitPct = null; if (tvProfitRaw !== null && baselineProfit > 0) { tvProfitPct = (tvProfitRaw / baselineProfit) * 100; } else if (tvProfitRaw !== null && tvProfitRaw > 0 && baselineProfit <= 0){ tvProfitPct = Infinity; } itemData['gPerprofit'] = (tvCost !== null && tvProfitPct !== null && isFinite(tvProfitPct) && tvProfitPct > 0) ? (tvCost / (tvProfitPct * 100)) : null; if (itemData['gPerprofit'] === null) { if (tvCost === 0 && tvProfitPct !== null && tvProfitPct > 0) itemData['gPerprofit_text'] = "Free"; else if (tvCost !== null && tvCost > 0 && tvProfitPct === Infinity) itemData['gPerprofit_text'] = "0"; else itemData['gPerprofit_text'] = "N/A"; } else { itemData['gPerprofit_text'] = itemData['gPerprofit'].toString(); // Use raw number for CSV } const tvDpsRaw = itemData['dpsChange']; let tvDpsPct = null; if (tvDpsRaw !== null && baselineDps > 0) { tvDpsPct = (tvDpsRaw / baselineDps) * 100; } else if (tvDpsRaw !== null && tvDpsRaw > 0 && baselineDps <= 0){ tvDpsPct = Infinity; } itemData['gPerdps'] = (tvCost !== null && tvDpsPct !== null && isFinite(tvDpsPct) && tvDpsPct > 0) ? (tvCost / (tvDpsPct * 100)) : null; if (itemData['gPerdps'] === null) { if (tvCost === 0 && tvDpsPct !== null && tvDpsPct > 0) itemData['gPerdps_text'] = "Free"; else if (tvCost !== null && tvCost > 0 && tvDpsPct === Infinity) itemData['gPerdps_text'] = "0"; else itemData['gPerdps_text'] = "N/A"; } else { itemData['gPerdps_text'] = itemData['gPerdps'].toString(); } const tvExpRaw = itemData['expChange']; let tvExpPct = null; if (tvExpRaw !== null && baselineExp > 0) { tvExpPct = (tvExpRaw / baselineExp) * 100; } else if (tvExpRaw !== null && tvExpRaw > 0 && baselineExp <= 0){ tvExpPct = Infinity; } itemData['gPerexpHr'] = (tvCost !== null && tvExpPct !== null && isFinite(tvExpPct) && tvExpPct > 0) ? (tvCost / (tvExpPct * 100)) : null; if (itemData['gPerexpHr'] === null) { if (tvCost === 0 && tvExpPct !== null && tvExpPct > 0) itemData['gPerexpHr_text'] = "Free"; else if (tvCost !== null && tvCost > 0 && tvExpPct === Infinity) itemData['gPerexpHr_text'] = "0"; else itemData['gPerexpHr_text'] = "N/A"; } else { itemData['gPerexpHr_text'] = itemData['gPerexpHr'].toString(); } const tvEphRaw = itemData['ephChange']; let tvEphPct = null; if (tvEphRaw !== null && baselineEph > 0) { tvEphPct = (tvEphRaw / baselineEph) * 100; } else if (tvEphRaw !== null && tvEphRaw > 0 && baselineEph <= 0){ tvEphPct = Infinity; } itemData['gPereph'] = (tvCost !== null && tvEphPct !== null && isFinite(tvEphPct) && tvEphPct > 0) ? (tvCost / (tvEphPct * 100)) : null; if (itemData['gPereph'] === null) { if (tvCost === 0 && tvEphPct !== null && tvEphPct > 0) itemData['gPereph_text'] = "Free"; else if (tvCost !== null && tvCost > 0 && tvEphPct === Infinity) itemData['gPereph_text'] = "0"; else itemData['gPereph_text'] = "N/A"; } else { itemData['gPereph_text'] = itemData['gPereph'].toString(); } let roi = null; if (tvCost !== null && tvProfitRaw !== null && tvProfitRaw > 0) { if (tvCost > 0) { const gainPerYear = tvProfitRaw * 24 * 365; roi = (gainPerYear / tvCost) * 100; } else if (tvCost === 0) { roi = Infinity; } } itemData['roi_text'] = formatValue(roi, 'roi'); // --- END MODIFICATION --- 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['cost_text']}","${item['timeToPurchase_text']}"`; for (const metricKey in METRIC_CONFIG) { if (METRIC_CONFIG[metricKey].hidden) continue; row += `,"${item[metricKey + '_text']}"`; if (metricKey.endsWith('Change')) { const newKey = `gPer${metricKey.replace('Change', '')}`; row += `,"${item[newKey + '_text']}"`; } } row += `,"${item['roi_text']}"\n`; csv += row; } } else { if (!METRIC_CONFIG[currentMetric]) return; 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) { 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')) { console.log("JIGS Stats: Waiting for document body and JIGS table..."); setTimeout(initializeWhenReady, 200); return; } // --- REMOVED: Do not call getBaselineStats() on initial load --- // getBaselineStats(); // <-- This was the problem line const riggerPanel = document.createElement('div'); riggerPanel.id = 'jig-rigger-panel'; let metricSelectorsHTML = '<div id="jr-metric-selector">'; for (const key in METRIC_CONFIG) { if (METRIC_CONFIG[key].hidden) continue; 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> `; 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+3) { text-align: right; } #rigger-results-table.jigs-summary-table td.jigs-tv-raw-data { background-color: rgba(0,0,0,0.2); } #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; } #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; } .rank-2-box { background-color: #4682B4; color: #fff; } .rank-3-box { background-color: #DAA520; } .rank-4-box { background-color: #CD853F; } .rank-5-box { background-color: #808080; color: #fff; } #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; } .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'; } } }); }); 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(); }); 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); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeWhenReady); } else { initializeWhenReady(); } })();