| |
| let allModels = []; |
| let filteredModels = []; |
|
|
| |
| 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; |
| } |
|
|
| |
| 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); |
| } |
| } |
|
|
| |
| let currentSort = { |
| column: 'output_input_multiple', |
| direction: 'desc' |
| }; |
|
|
| |
| function initializeApp() { |
| updateStats(); |
| populateTable(); |
| setupTableControls(); |
| setupSorting(); |
| createMultipleQuadrantChart(); |
| createDistributionChart(); |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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'; |
| } |
|
|
| |
| 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'; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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 ` |
| <tr class="${vendorClass}" data-vendor="${m.displayVendor}"> |
| <td>${cleanModelName(m.model_name, m.displayVendor)}</td> |
| <td class="vendor-cell">${m.displayVendor}</td> |
| <td class="${getCostTier(m.input_price_usd_per_m)}" style="font-weight: 600;"> |
| $${m.input_price_usd_per_m.toFixed(2)} |
| </td> |
| <td class="${getCostTier(m.output_price_usd_per_m)}" style="font-weight: 600;"> |
| $${m.output_price_usd_per_m.toFixed(2)} |
| </td> |
| <td class="${getMultipleTier(m.output_input_multiple)}" style="font-weight: 600;"> |
| ${m.output_input_multiple.toFixed(2)}x |
| </td> |
| <td class="${getCostTier(m.avg_cost)}" style="font-weight: 600;"> |
| $${m.avg_cost.toFixed(2)} |
| </td> |
| </tr> |
| `; |
| }).join(''); |
| } |
|
|
| |
| 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); |
| }); |
| }); |
|
|
| |
| const initialHeader = document.querySelector(`th[data-sort="${currentSort.column}"]`); |
| if (initialHeader) { |
| initialHeader.classList.add('sort-desc'); |
| } |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| 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); |
|
|
| |
| 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']] |
| }); |
| } |
|
|
| |
| 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', |
| '#84cc16', |
| '#fde047', |
| '#f97316', |
| '#ef4444' |
| ], |
| 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' |
| } |
| } |
| } |
| } |
| }); |
| } |
|
|
| |
| document.addEventListener('DOMContentLoaded', loadData); |
|
|