|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
const apiUrl = 'https://badimo.nyc3.digitaloceanspaces.com/trade/frequency/snapshot/month/latest.json';
|
|
|
|
|
|
let tradingData = [];
|
|
let currentSortColumn = 'DemandMultiple';
|
|
let currentSortDirection = 'desc';
|
|
let demandChart = null;
|
|
let typeChart = null;
|
|
|
|
|
|
async function fetchData() {
|
|
try {
|
|
const response = await fetch(apiUrl);
|
|
if (!response.ok) {
|
|
throw new Error('Network response was not ok');
|
|
}
|
|
tradingData = await response.json();
|
|
|
|
|
|
const now = new Date();
|
|
document.getElementById('lastUpdated').textContent = `Data as of ${now.toLocaleDateString()} ${now.toLocaleTimeString()}`;
|
|
|
|
|
|
processData();
|
|
|
|
|
|
updateStatistics();
|
|
populateTypeFilter();
|
|
renderTable();
|
|
createCharts();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to fetch data:', error);
|
|
document.getElementById('tableBody').innerHTML = `
|
|
<tr>
|
|
<td colspan="5" class="text-center py-4 text-red-500">
|
|
Error loading data. Please try again later.
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
}
|
|
|
|
|
|
function processData() {
|
|
|
|
|
|
sortData(currentSortColumn, currentSortDirection);
|
|
}
|
|
|
|
|
|
function updateStatistics() {
|
|
const totalItems = tradingData.length;
|
|
const totalTrades = tradingData.reduce((sum, item) => sum + item.TimesTraded, 0);
|
|
const avgDemandMultiple = (tradingData.reduce((sum, item) => sum + item.DemandMultiple, 0) / totalItems).toFixed(2);
|
|
const totalCirculation = tradingData.reduce((sum, item) => sum + item.UniqueCirculation, 0);
|
|
|
|
document.getElementById('totalItems').textContent = totalItems.toLocaleString();
|
|
document.getElementById('totalTrades').textContent = totalTrades.toLocaleString();
|
|
document.getElementById('avgDemandMultiple').textContent = avgDemandMultiple;
|
|
document.getElementById('totalCirculation').textContent = totalCirculation.toLocaleString();
|
|
}
|
|
|
|
|
|
function populateTypeFilter() {
|
|
const types = [...new Set(tradingData.map(item => item.Type))];
|
|
const select = document.getElementById('typeFilter');
|
|
|
|
types.forEach(type => {
|
|
const option = document.createElement('option');
|
|
option.value = type;
|
|
option.textContent = type;
|
|
select.appendChild(option);
|
|
});
|
|
}
|
|
|
|
|
|
function sortData(column, direction) {
|
|
tradingData.sort((a, b) => {
|
|
let comparison = 0;
|
|
|
|
if (typeof a[column] === 'string') {
|
|
comparison = a[column].localeCompare(b[column]);
|
|
} else {
|
|
comparison = a[column] - b[column];
|
|
}
|
|
|
|
return direction === 'asc' ? comparison : -comparison;
|
|
});
|
|
}
|
|
|
|
|
|
function renderTable() {
|
|
const tableBody = document.getElementById('tableBody');
|
|
const searchInput = document.getElementById('searchInput').value.toLowerCase();
|
|
const typeFilter = document.getElementById('typeFilter').value;
|
|
|
|
|
|
const filteredData = tradingData.filter(item => {
|
|
const nameMatch = item.Name.toLowerCase().includes(searchInput);
|
|
const typeMatch = typeFilter === 'all' || item.Type === typeFilter;
|
|
return nameMatch && typeMatch;
|
|
});
|
|
|
|
|
|
tableBody.innerHTML = '';
|
|
|
|
|
|
if (filteredData.length === 0) {
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="5" class="text-center py-4">No matching results found</td>
|
|
</tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
filteredData.forEach(item => {
|
|
const row = document.createElement('tr');
|
|
row.className = 'border-b border-gray-200 hover:bg-gray-50';
|
|
|
|
|
|
const demandLevel = getDemandLevel(item.DemandMultiple);
|
|
|
|
row.innerHTML = `
|
|
<td class="py-3 px-6 text-left">${item.Name}</td>
|
|
<td class="py-3 px-6 text-left">${formatType(item.Type)}</td>
|
|
<td class="py-3 px-6 text-right">${item.TimesTraded.toLocaleString()}</td>
|
|
<td class="py-3 px-6 text-right">${item.UniqueCirculation.toLocaleString()}</td>
|
|
<td class="py-3 px-6 text-right">
|
|
<span class="px-2 py-1 rounded ${demandLevel.color} text-white">
|
|
${item.DemandMultiple.toFixed(2)}
|
|
</span>
|
|
</td>
|
|
`;
|
|
|
|
tableBody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
|
|
function formatType(type) {
|
|
if (type.includes('.')) {
|
|
return type.split('.').pop();
|
|
}
|
|
return type;
|
|
}
|
|
|
|
|
|
function getDemandLevel(demand) {
|
|
if (demand >= 5) return { level: 'Very High', color: 'bg-red-600' };
|
|
if (demand >= 3) return { level: 'High', color: 'bg-orange-500' };
|
|
if (demand >= 2) return { level: 'Medium', color: 'bg-yellow-500' };
|
|
if (demand >= 1) return { level: 'Low', color: 'bg-green-500' };
|
|
return { level: 'Very Low', color: 'bg-blue-500' };
|
|
}
|
|
|
|
|
|
function createCharts() {
|
|
createDemandChart();
|
|
createTypeDistributionChart();
|
|
}
|
|
|
|
|
|
function createDemandChart() {
|
|
const top10ByDemand = [...tradingData]
|
|
.sort((a, b) => b.DemandMultiple - a.DemandMultiple)
|
|
.slice(0, 10);
|
|
|
|
const ctx = document.getElementById('demandChart').getContext('2d');
|
|
|
|
if (demandChart) {
|
|
demandChart.destroy();
|
|
}
|
|
|
|
demandChart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: top10ByDemand.map(item => item.Name),
|
|
datasets: [{
|
|
label: 'Demand Multiple',
|
|
data: top10ByDemand.map(item => item.DemandMultiple),
|
|
backgroundColor: 'rgba(99, 102, 241, 0.7)',
|
|
borderColor: 'rgb(79, 70, 229)',
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: 'Demand Multiple'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
function createTypeDistributionChart() {
|
|
const typeDistribution = {};
|
|
tradingData.forEach(item => {
|
|
const type = formatType(item.Type);
|
|
if (!typeDistribution[type]) {
|
|
typeDistribution[type] = 0;
|
|
}
|
|
typeDistribution[type]++;
|
|
});
|
|
|
|
const ctx = document.getElementById('typeChart').getContext('2d');
|
|
|
|
if (typeChart) {
|
|
typeChart.destroy();
|
|
}
|
|
|
|
const colors = [
|
|
'rgba(99, 102, 241, 0.7)',
|
|
'rgba(239, 68, 68, 0.7)',
|
|
'rgba(16, 185, 129, 0.7)',
|
|
'rgba(245, 158, 11, 0.7)',
|
|
'rgba(139, 92, 246, 0.7)',
|
|
'rgba(14, 165, 233, 0.7)'
|
|
];
|
|
|
|
typeChart = new Chart(ctx, {
|
|
type: 'pie',
|
|
data: {
|
|
labels: Object.keys(typeDistribution),
|
|
datasets: [{
|
|
label: 'Item Count',
|
|
data: Object.values(typeDistribution),
|
|
backgroundColor: colors,
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'right',
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
function setupEventListeners() {
|
|
|
|
document.querySelectorAll('th[data-sort]').forEach(header => {
|
|
header.addEventListener('click', () => {
|
|
const column = header.dataset.sort;
|
|
|
|
|
|
const direction =
|
|
column === currentSortColumn && currentSortDirection === 'asc' ? 'desc' : 'asc';
|
|
|
|
|
|
currentSortColumn = column;
|
|
currentSortDirection = direction;
|
|
|
|
|
|
updateSortIcons();
|
|
sortData(column, direction);
|
|
renderTable();
|
|
});
|
|
});
|
|
|
|
|
|
document.getElementById('searchInput').addEventListener('input', renderTable);
|
|
|
|
|
|
document.getElementById('typeFilter').addEventListener('change', renderTable);
|
|
}
|
|
|
|
|
|
function updateSortIcons() {
|
|
document.querySelectorAll('th[data-sort]').forEach(header => {
|
|
const icon = header.querySelector('.sort-icon');
|
|
if (header.dataset.sort === currentSortColumn) {
|
|
icon.textContent = currentSortDirection === 'asc' ? ' ↑' : ' ↓';
|
|
header.classList.add('font-bold');
|
|
} else {
|
|
icon.textContent = '';
|
|
header.classList.remove('font-bold');
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
fetchData();
|
|
setupEventListeners();
|
|
updateSortIcons();
|
|
});
|
|
|