// Global data storage let allModels = []; let filteredModels = []; // Vendor name mapping (same as main app) const vendorNameMap = { 'qwen': 'Qwen', 'meta-llama': 'Meta', 'x-ai': 'xAI', 'z-ai': 'Zhipu AI', 'google': 'Google', 'openai': 'OpenAI', 'anthropic': 'Anthropic', 'mistralai': 'Mistral AI', 'deepseek': 'DeepSeek', 'alibaba': 'Alibaba', 'amazon': 'Amazon', 'microsoft': 'Microsoft', 'nvidia': 'NVIDIA', 'cohere': 'Cohere', 'ai21': 'AI21 Labs', 'minimax': 'MiniMax', 'moonshotai': 'Moonshot AI', 'stepfun-ai': 'StepFun', 'inclusionai': 'Inclusion AI', 'deepcogito': 'Deep Cogito', 'baidu': 'Baidu', 'nousresearch': 'Nous Research', 'arcee-ai': 'Arcee AI', 'inception': 'Inception', 'sao10k': 'Sao10K', 'thedrummer': 'TheDrummer', 'tngtech': 'TNG Technology', 'meituan': 'Meituan' }; function normalizeVendorName(vendor) { return vendorNameMap[vendor] || vendor; } // Load and process CSV data async function loadData() { try { const response = await fetch('quadrants.csv'); const csvText = await response.text(); Papa.parse(csvText, { header: true, dynamicTyping: true, complete: function(results) { allModels = results.data.filter(row => row.model_name); allModels.forEach(model => { model.displayVendor = normalizeVendorName(model.vendor); }); filteredModels = [...allModels]; initializeApp(); } }); } catch (error) { console.error('Error loading data:', error); } } // Sorting state let currentSort = { column: 'output_input_multiple', direction: 'desc' // Start with highest multiples first }; // Initialize application function initializeApp() { updateStats(); populateTable(); setupTableControls(); setupSorting(); createMultipleQuadrantChart(); createDistributionChart(); } // Calculate median function median(arr) { const sorted = [...arr].sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; } // Update statistics function updateStats() { const multiples = allModels.map(m => m.output_input_multiple); const medianMultiple = median(multiples); const equalPricing = allModels.filter(m => m.output_input_multiple <= 1).length; const highMultiple = allModels.filter(m => m.output_input_multiple >= 5).length; document.getElementById('total-models').textContent = allModels.length; document.getElementById('median-multiple').textContent = `${medianMultiple.toFixed(2)}x`; document.getElementById('median-multiple-stat').textContent = `${medianMultiple.toFixed(2)}x`; document.getElementById('median-line-value').textContent = `${medianMultiple.toFixed(2)}x`; document.getElementById('equal-pricing-count').textContent = equalPricing; document.getElementById('high-multiple-count').textContent = highMultiple; } // Get tier class for cost function getCostTier(cost) { if (cost < 0.10) return 'cost-very-low'; if (cost < 0.50) return 'cost-low'; if (cost < 2.00) return 'cost-medium'; if (cost < 40.00) return 'cost-high'; return 'cost-very-high'; } // Get tier class for output/input multiple function getMultipleTier(multiple) { if (multiple <= 1) return 'multiple-equal'; if (multiple < 2) return 'multiple-low'; if (multiple < 5) return 'multiple-medium'; if (multiple < 10) return 'multiple-high'; return 'multiple-very-high'; } // Clean model name function cleanModelName(modelName, vendor) { const patterns = [ new RegExp(`^${vendor}:\\s*`, 'i'), /^Qwen:\s*/i, /^Meta:\s*/i, /^Google:\s*/i, /^OpenAI:\s*/i, /^Anthropic:\s*/i, /^DeepSeek:\s*/i, /^Mistral:\s*/i, /^NVIDIA:\s*/i, /^Amazon:\s*/i, /^Microsoft:\s*/i, /^xAI:\s*/i, /^Zhipu AI:\s*/i, /^MoonshotAI:\s*/i, /^Moonshot AI:\s*/i, /^Alibaba:\s*/i ]; let cleaned = modelName; for (const pattern of patterns) { cleaned = cleaned.replace(pattern, ''); } return cleaned; } // Populate table function populateTable(models = filteredModels) { const tbody = document.getElementById('table-body'); const sortedModels = [...models].sort((a, b) => { let aVal = a[currentSort.column]; let bVal = b[currentSort.column]; if (typeof aVal === 'string') { aVal = aVal.toLowerCase(); bVal = bVal.toLowerCase(); } if (currentSort.direction === 'asc') { return aVal > bVal ? 1 : -1; } else { return aVal < bVal ? 1 : -1; } }); let lastVendor = null; tbody.innerHTML = sortedModels.map((m, index) => { const isNewVendor = m.displayVendor !== lastVendor; lastVendor = m.displayVendor; const vendorClass = isNewVendor ? 'vendor-group-start' : ''; return ` ${cleanModelName(m.model_name, m.displayVendor)} ${m.displayVendor} $${m.input_price_usd_per_m.toFixed(2)} $${m.output_price_usd_per_m.toFixed(2)} ${m.output_input_multiple.toFixed(2)}x $${m.avg_cost.toFixed(2)} `; }).join(''); } // Setup sorting function setupSorting() { const headers = document.querySelectorAll('th.sortable'); headers.forEach(header => { header.addEventListener('click', () => { const column = header.dataset.sort; if (currentSort.column === column) { currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc'; } else { currentSort.column = column; currentSort.direction = 'asc'; } headers.forEach(h => { h.classList.remove('sort-asc', 'sort-desc'); }); header.classList.add(`sort-${currentSort.direction}`); populateTable(filteredModels); }); }); // Set initial sort indicator const initialHeader = document.querySelector(`th[data-sort="${currentSort.column}"]`); if (initialHeader) { initialHeader.classList.add('sort-desc'); } } // Setup table controls function setupTableControls() { const searchInput = document.getElementById('search'); const multipleFilter = document.getElementById('multiple-filter'); function applyFilters() { const searchTerm = searchInput.value.toLowerCase(); const multipleCategory = multipleFilter.value; filteredModels = allModels.filter(m => { const matchesSearch = !searchTerm || m.model_name.toLowerCase().includes(searchTerm) || m.displayVendor.toLowerCase().includes(searchTerm); let matchesMultiple = true; if (multipleCategory === 'equal') matchesMultiple = m.output_input_multiple <= 1; else if (multipleCategory === 'low') matchesMultiple = m.output_input_multiple < 2; else if (multipleCategory === 'medium') matchesMultiple = m.output_input_multiple >= 2 && m.output_input_multiple < 5; else if (multipleCategory === 'high') matchesMultiple = m.output_input_multiple >= 5 && m.output_input_multiple < 10; else if (multipleCategory === 'very-high') matchesMultiple = m.output_input_multiple >= 10; return matchesSearch && matchesMultiple; }); populateTable(filteredModels); } searchInput.addEventListener('input', applyFilters); multipleFilter.addEventListener('change', applyFilters); } // Create quadrant chart: Output/Input Multiple vs Average Cost function createMultipleQuadrantChart() { const ctx = document.getElementById('multipleQuadrantChart').getContext('2d'); const multiples = allModels.map(m => m.output_input_multiple); const costs = allModels.map(m => m.avg_cost); const medianMultiple = median(multiples); const medianCost = median(costs); // Divide into quadrants const quadrants = { 'Low Multiple / Low Cost': { color: '#10b981', models: [] }, 'High Multiple / Low Cost': { color: '#f59e0b', models: [] }, 'Low Multiple / High Cost': { color: '#2563eb', models: [] }, 'High Multiple / High Cost': { color: '#ef4444', models: [] } }; allModels.forEach(m => { const isLowMultiple = m.output_input_multiple < medianMultiple; const isLowCost = m.avg_cost < medianCost; if (isLowMultiple && isLowCost) quadrants['Low Multiple / Low Cost'].models.push(m); else if (!isLowMultiple && isLowCost) quadrants['High Multiple / Low Cost'].models.push(m); else if (isLowMultiple && !isLowCost) quadrants['Low Multiple / High Cost'].models.push(m); else quadrants['High Multiple / High Cost'].models.push(m); }); const datasets = Object.keys(quadrants).map(quadrant => { const q = quadrants[quadrant]; return { label: quadrant, data: q.models.map(m => ({ x: m.output_input_multiple, y: m.avg_cost, model: m })), backgroundColor: q.color + '80', borderColor: q.color, borderWidth: 2, pointRadius: 5, pointHoverRadius: 8 }; }); new Chart(ctx, { type: 'scatter', data: { datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'top' }, tooltip: { callbacks: { label: function(context) { const model = context.raw.model; return [ model.model_name, `Vendor: ${model.displayVendor}`, `Out/In Multiple: ${model.output_input_multiple.toFixed(2)}x`, `Input: $${model.input_price_usd_per_m.toFixed(2)}/M`, `Output: $${model.output_price_usd_per_m.toFixed(2)}/M`, `Avg: $${model.avg_cost.toFixed(2)}/M` ]; } } }, annotation: { annotations: { verticalLine: { type: 'line', xMin: medianMultiple, xMax: medianMultiple, borderColor: '#64748b', borderWidth: 2, borderDash: [5, 5], label: { display: true, content: `Median Multiple: ${medianMultiple.toFixed(2)}x`, position: 'start' } }, horizontalLine: { type: 'line', yMin: medianCost, yMax: medianCost, borderColor: '#64748b', borderWidth: 2, borderDash: [5, 5], label: { display: true, content: `Median Cost: $${medianCost.toFixed(2)}`, position: 'end' } } } } }, scales: { x: { title: { display: true, text: 'Output/Input Price Multiple' } }, y: { type: 'logarithmic', title: { display: true, text: 'Average Cost ($/M tokens, log scale)' } } } }, plugins: [window['chartjs-plugin-annotation']] }); } // Create distribution bar chart function createDistributionChart() { const ctx = document.getElementById('multipleDistribution').getContext('2d'); const bins = { '≤1x (Equal)': allModels.filter(m => m.output_input_multiple <= 1).length, '<2x (Low)': allModels.filter(m => m.output_input_multiple > 1 && m.output_input_multiple < 2).length, '2-5x (Medium)': allModels.filter(m => m.output_input_multiple >= 2 && m.output_input_multiple < 5).length, '5-10x (High)': allModels.filter(m => m.output_input_multiple >= 5 && m.output_input_multiple < 10).length, '10x+ (Very High)': allModels.filter(m => m.output_input_multiple >= 10).length }; new Chart(ctx, { type: 'bar', data: { labels: Object.keys(bins), datasets: [{ label: 'Number of Models', data: Object.values(bins), backgroundColor: [ '#10b981', // Equal '#84cc16', // Low '#fde047', // Medium '#f97316', // High '#ef4444' // Very High ], borderColor: [ '#059669', '#65a30d', '#eab308', '#ea580c', '#dc2626' ], borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { label: function(context) { const percentage = ((context.parsed.y / allModels.length) * 100).toFixed(1); return `${context.parsed.y} models (${percentage}%)`; } } } }, scales: { y: { beginAtZero: true, title: { display: true, text: 'Number of Models' } }, x: { title: { display: true, text: 'Output/Input Multiple Range' } } } } }); } // Initialize on page load document.addEventListener('DOMContentLoaded', loadData);