251 lines
8.1 KiB
JavaScript
251 lines
8.1 KiB
JavaScript
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);
|
|
|
|
// Always build the full date axis for the selected window, even if sparse.
|
|
// Generate one label per day in the range so the x-axis reflects the
|
|
// chosen period rather than collapsing to only the days that have data.
|
|
const dataDateSet = new Set(filtered.map(r => r.calculatedAt));
|
|
const allDates = [...dataDateSet].sort((a, b) => new Date(a) - new Date(b));
|
|
|
|
// If we have real data, expand the axis to span from cutoff → today so
|
|
// empty stretches at the start/end of a range are visible.
|
|
let axisLabels = allDates;
|
|
if (allDates.length > 0 && RANGE_DAYS[rangeKey] !== Infinity) {
|
|
const start = new Date(cutoff);
|
|
const end = new Date();
|
|
const expanded = [];
|
|
// Step through every day in the window
|
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
|
expanded.push(d.toISOString().split('T')[0]);
|
|
}
|
|
axisLabels = expanded;
|
|
}
|
|
|
|
const labels = axisLabels.map(formatDate);
|
|
|
|
const lookup = {};
|
|
for (const row of filtered) {
|
|
if (!lookup[row.condition]) lookup[row.condition] = {};
|
|
lookup[row.condition][row.calculatedAt] = Number(row.marketPrice);
|
|
}
|
|
|
|
const activeConditionHasData = allDates.some(
|
|
date => lookup[activeCondition]?.[date] != null
|
|
);
|
|
|
|
const datasets = CONDITIONS.map(condition => {
|
|
const isActive = condition === activeCondition;
|
|
const colors = CONDITION_COLORS[condition];
|
|
const data = axisLabels.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);
|
|
|
|
// Always push the new labels/datasets to the chart so the x-axis
|
|
// reflects the selected time window — even when there's no data for
|
|
// the active condition. Then toggle the empty state overlay on top.
|
|
chartInstance.data.labels = labels;
|
|
chartInstance.data.datasets = datasets;
|
|
chartInstance.update('none');
|
|
|
|
// Show the empty state overlay if the active condition has no points
|
|
// in this window, but leave the (empty) chart visible underneath so
|
|
// the axis communicates the selected period.
|
|
setEmptyState(!hasData || !activeConditionHasData);
|
|
}
|
|
|
|
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);
|
|
|
|
// Render the chart regardless — show empty state overlay if needed
|
|
setEmptyState(!hasData || !activeConditionHasData);
|
|
|
|
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(); |