// Global data storage let allModels = []; let filteredModels = []; // Vendor name mapping 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); // Normalize vendor names allModels.forEach(model => { model.displayVendor = normalizeVendorName(model.vendor); }); filteredModels = [...allModels]; initializeApp(); } }); } catch (error) { console.error('Error loading data:', error); } } // Sorting state let currentSort = { column: 'model_name', direction: 'asc' }; // Initialize all visualizations and features function initializeApp() { updateStats(); populateTable(); setupTableControls(); setupSorting(); setupTabs(); createQuadrantChart(); createAllQuadrantsChart(); createIndividualQuadrantCharts(); } // Setup tab navigation function setupTabs() { const tabButtons = document.querySelectorAll('.tab-button'); const tabContents = document.querySelectorAll('.tab-content'); tabButtons.forEach(button => { button.addEventListener('click', () => { const targetTab = button.dataset.tab; // Remove active class from all buttons and contents tabButtons.forEach(btn => btn.classList.remove('active')); tabContents.forEach(content => content.classList.remove('active')); // Add active class to clicked button and corresponding content button.classList.add('active'); document.getElementById(`tab-${targetTab}`).classList.add('active'); }); }); } // Update summary statistics function updateStats() { const costs = allModels.map(m => m.avg_cost); const contexts = allModels.map(m => m.context_length); const inputPrices = allModels.map(m => m.input_price_usd_per_m); const outputPrices = allModels.map(m => m.output_price_usd_per_m); const multiples = allModels.map(m => m.output_input_multiple); const minContext = Math.min(...contexts); const maxContext = Math.max(...contexts); const vendors = new Set(allModels.map(m => m.displayVendor)).size; // Calculate medians const medianCost = median(costs); const medianContext = median(contexts); const medianInput = median(inputPrices); const medianOutput = median(outputPrices); const medianMultiple = median(multiples); document.getElementById('total-models').textContent = allModels.length; document.getElementById('vendors').textContent = vendors; document.getElementById('median-input').textContent = `$${medianInput.toFixed(2)}`; document.getElementById('median-output').textContent = `$${medianOutput.toFixed(2)}`; document.getElementById('median-multiple').textContent = `${medianMultiple.toFixed(2)}x`; document.getElementById('context-range').textContent = `${(minContext/1000).toFixed(0)}K - ${(maxContext/1000000).toFixed(1)}M`; // Update methodology section with actual median values document.getElementById('median-cost-display').textContent = `$${medianCost.toFixed(2)}/M tokens`; document.getElementById('median-context-display').textContent = `${(medianContext/1000).toFixed(0)}K tokens`; } // Calculate median for quadrant lines 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; } // Create main quadrant scatter plot with division lines function createQuadrantChart() { const ctx = document.getElementById('quadrantChart').getContext('2d'); const quadrantColors = { 'Low Cost / High Context': '#10b981', 'High Cost / High Context': '#2563eb', 'Low Cost / Low Context': '#f59e0b', 'High Cost / Low Context': '#ef4444' }; const datasets = Object.keys(quadrantColors).map(quadrant => { const models = allModels.filter(m => m.quadrant === quadrant); return { label: quadrant, data: models.map(m => ({ x: m.avg_cost, y: m.context_length / 1000, model: m })), backgroundColor: quadrantColors[quadrant] + '80', borderColor: quadrantColors[quadrant], borderWidth: 2, pointRadius: 5, pointHoverRadius: 8 }; }); // Calculate medians for quadrant lines const medianCost = median(allModels.map(m => m.avg_cost)); const medianContext = median(allModels.map(m => m.context_length / 1000)); new Chart(ctx, { type: 'scatter', data: { datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: false }, legend: { display: true, position: 'top' }, tooltip: { callbacks: { label: function(context) { const model = context.raw.model; return [ model.model_name, `Vendor: ${model.displayVendor}`, `Context: ${(model.context_length / 1000).toFixed(0)}K tokens`, `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: medianCost, xMax: medianCost, borderColor: '#64748b', borderWidth: 2, borderDash: [5, 5], label: { display: true, content: `Median Cost: $${medianCost.toFixed(2)}`, position: 'start' } }, horizontalLine: { type: 'line', yMin: medianContext, yMax: medianContext, borderColor: '#64748b', borderWidth: 2, borderDash: [5, 5], label: { display: true, content: `Median Context: ${medianContext.toFixed(0)}K`, position: 'end' } } } } }, scales: { x: { type: 'logarithmic', title: { display: true, text: 'Average Cost ($/M tokens, log scale)' }, min: 0.01 }, y: { title: { display: true, text: 'Context Window (K tokens)' } } } }, plugins: [window['chartjs-plugin-annotation']] }); } // 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'; // $40+ per million tokens } // Get tier class for context function getContextTier(context) { if (context < 32000) return 'context-small'; if (context < 128000) return 'context-medium'; if (context < 256000) return 'context-large'; if (context < 1000000) return 'context-very-large'; return 'context-ultra'; } // Get tier class for output/input multiple function getMultipleTier(multiple) { if (multiple <= 1) return 'multiple-equal'; // Equal or cheaper output if (multiple < 2) return 'multiple-low'; // Less than 2x if (multiple < 5) return 'multiple-medium'; // 2-5x (most common) if (multiple < 10) return 'multiple-high'; // 5-10x return 'multiple-very-high'; // 10x+ } // Clean up model name by removing redundant vendor prefix function cleanModelName(modelName, vendor) { // Common patterns to remove 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 data table with color coding and vendor grouping function populateTable(models = filteredModels) { const tbody = document.getElementById('table-body'); // Sort models const sortedModels = [...models].sort((a, b) => { let aVal = a[currentSort.column]; let bVal = b[currentSort.column]; // Handle string comparisons 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 `