// ==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();
}
})();