import Chart from 'chart.js/auto'; const CONDITIONS = ["Near Mint", "Lightly Played", "Moderately Played", "Heavily Played", "Damaged"]; const CONDITION_COLORS = { "Near Mint": { active: 'rgba(156, 204, 102, 1)', muted: 'rgba(156, 204, 102, 0.67)' }, "Lightly Played": { active: 'rgba(211, 225, 86, 1)', muted: 'rgba(211, 225, 86, 0.67)' }, "Moderately Played": { active: 'rgba(255, 238, 87, 1)', muted: 'rgba(255, 238, 87, 0.67)' }, "Heavily Played": { active: 'rgba(255, 201, 41, 1)', muted: 'rgba(255, 201, 41, 0.67)' }, "Damaged": { active: 'rgba(255, 167, 36, 1)', muted: 'rgba(255, 167, 36, 0.67)' }, }; const RANGE_DAYS = { '1m': 30, '3m': 90, '6m': 180, '1y': 365, 'all': Infinity }; let chartInstance = null; let allHistory = []; let activeCondition = "Near Mint"; let activeRange = '1m'; function formatDate(dateStr) { const [year, month, day] = dateStr.split('-'); const d = new Date(Number(year), Number(month) - 1, Number(day)); return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } function setEmptyState(isEmpty) { const modal = document.getElementById('cardModal'); const empty = modal?.querySelector('#priceHistoryEmpty'); const canvasWrapper = empty?.nextElementSibling; if (!empty || !canvasWrapper) return; empty.classList.toggle('d-none', !isEmpty); canvasWrapper.classList.toggle('d-none', isEmpty); } function buildChartData(history, rangeKey) { const cutoff = RANGE_DAYS[rangeKey] === Infinity ? new Date(0) : new Date(Date.now() - RANGE_DAYS[rangeKey] * 86_400_000); const filtered = history.filter(r => new Date(r.calculatedAt) >= cutoff); const allDates = [...new Set(filtered.map(r => r.calculatedAt))] .sort((a, b) => new Date(a) - new Date(b)); const labels = allDates.map(formatDate); const lookup = {}; for (const row of filtered) { if (!lookup[row.condition]) lookup[row.condition] = {}; lookup[row.condition][row.calculatedAt] = Number(row.marketPrice); } // Check specifically whether the active condition has any data points const activeConditionDates = allDates.filter( date => lookup[activeCondition]?.[date] != null ); const activeConditionHasData = activeConditionDates.length > 0; const datasets = CONDITIONS.map(condition => { const isActive = condition === activeCondition; const colors = CONDITION_COLORS[condition]; const data = allDates.map(date => lookup[condition]?.[date] ?? null); return { label: condition, data, borderColor: isActive ? colors.active : colors.muted, borderWidth: isActive ? 2 : 1, pointRadius: isActive ? 2.5 : 0, pointHoverRadius: isActive ? 5 : 3, pointBackgroundColor: isActive ? colors.active : colors.muted, tension: 0.3, fill: false, spanGaps: true, order: isActive ? 0 : 1, }; }); return { labels, datasets, hasData: allDates.length > 0, activeConditionHasData }; } function updateChart() { if (!chartInstance) return; const { labels, datasets, hasData, activeConditionHasData } = buildChartData(allHistory, activeRange); // Show empty state if no data at all, or if the active condition specifically has no data if (!hasData || !activeConditionHasData) { setEmptyState(true); return; } setEmptyState(false); chartInstance.data.labels = labels; chartInstance.data.datasets = datasets; chartInstance.update('none'); } function initPriceChart(canvas) { if (chartInstance) { chartInstance.destroy(); chartInstance = null; } try { allHistory = JSON.parse(canvas.dataset.history ?? '[]'); } catch (err) { console.error('Failed to parse price history:', err); return; } if (!allHistory.length) { setEmptyState(true); return; } const { labels, datasets, hasData, activeConditionHasData } = buildChartData(allHistory, activeRange); if (!hasData || !activeConditionHasData) { setEmptyState(true); return; } setEmptyState(false); chartInstance = new Chart(canvas.getContext('2d'), { type: 'line', data: { labels, datasets }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false, }, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(0, 0, 0, 0.85)', titleColor: 'rgba(255, 255, 255, 0.9)', bodyColor: 'rgba(255, 255, 255, 0.75)', borderColor: 'rgba(255, 255, 255, 0.1)', borderWidth: 1, padding: 10, callbacks: { labelColor: (ctx) => { const colors = CONDITION_COLORS[ctx.dataset.label]; return { borderColor: colors.active, backgroundColor: colors.active, }; }, label: (ctx) => { const isActive = ctx.dataset.label === activeCondition; const price = ctx.parsed.y != null ? `$${ctx.parsed.y.toFixed(2)}` : '—'; return isActive ? ` ${ctx.dataset.label}: ${price} ◀` : ` ${ctx.dataset.label}: ${price}`; } } } }, scales: { x: { grid: { color: 'rgba(255, 255, 255, 0.05)' }, ticks: { color: 'rgba(255, 255, 255, 0.4)', maxTicksLimit: 6, maxRotation: 0, }, border: { color: 'rgba(255, 255, 255, 0.1)' }, }, y: { grid: { color: 'rgba(255, 255, 255, 0.05)' }, ticks: { color: 'rgba(255, 255, 255, 0.4)', callback: (val) => `$${Number(val).toFixed(2)}`, }, border: { color: 'rgba(255, 255, 255, 0.1)' }, } } } }); } function initFromCanvas(canvas) { activeCondition = "Near Mint"; activeRange = '1m'; const modal = document.getElementById('cardModal'); modal?.querySelectorAll('.price-range-btn').forEach(b => { b.classList.toggle('active', b.dataset.range === '1m'); }); initPriceChart(canvas); } function setup() { const modal = document.getElementById('cardModal'); if (!modal) return; modal.addEventListener('card-modal:swapped', () => { const canvas = modal.querySelector('#priceHistoryChart'); if (canvas) initFromCanvas(canvas); }); modal.addEventListener('hidden.bs.modal', () => { if (chartInstance) { chartInstance.destroy(); chartInstance = null; } allHistory = []; }); document.addEventListener('shown.bs.tab', (e) => { if (!modal.contains(e.target)) return; const target = e.target?.getAttribute('data-bs-target'); const conditionMap = { '#nav-nm': 'Near Mint', '#nav-lp': 'Lightly Played', '#nav-mp': 'Moderately Played', '#nav-hp': 'Heavily Played', '#nav-dmg': 'Damaged', }; if (target && conditionMap[target]) { activeCondition = conditionMap[target]; updateChart(); } }); document.addEventListener('click', (e) => { const btn = e.target?.closest('.price-range-btn'); if (!btn || !modal.contains(btn)) return; modal.querySelectorAll('.price-range-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); activeRange = btn.dataset.range ?? '1m'; updateChart(); }); } setup();