| |
| 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: 'model_name', |
| direction: 'asc' |
| }; |
|
|
| |
| function initializeApp() { |
| updateStats(); |
| populateTable(); |
| setupTableControls(); |
| setupSorting(); |
| setupTabs(); |
| createQuadrantChart(); |
| createAllQuadrantsChart(); |
| createIndividualQuadrantCharts(); |
| } |
|
|
| |
| 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; |
|
|
| |
| tabButtons.forEach(btn => btn.classList.remove('active')); |
| tabContents.forEach(content => content.classList.remove('active')); |
|
|
| |
| button.classList.add('active'); |
| document.getElementById(`tab-${targetTab}`).classList.add('active'); |
| }); |
| }); |
| } |
|
|
| |
| 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; |
|
|
| |
| 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`; |
|
|
| |
| document.getElementById('median-cost-display').textContent = `$${medianCost.toFixed(2)}/M tokens`; |
| document.getElementById('median-context-display').textContent = `${(medianContext/1000).toFixed(0)}K tokens`; |
| } |
|
|
| |
| 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 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 |
| }; |
| }); |
|
|
| |
| 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']] |
| }); |
| } |
|
|
|
|
| |
| 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 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'; |
| } |
|
|
| |
| 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="${getContextTier(m.context_length)}" style="font-weight: 600;"> |
| ${(m.context_length / 1000).toFixed(0)}K |
| </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><span class="quadrant-badge">${m.quadrant}</span></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-asc'); |
| } |
| } |
|
|
| |
| function setupTableControls() { |
| const searchInput = document.getElementById('search'); |
| const quadrantFilter = document.getElementById('quadrant-filter'); |
|
|
| function applyFilters() { |
| const searchTerm = searchInput.value.toLowerCase(); |
| const quadrant = quadrantFilter.value; |
|
|
| filteredModels = allModels.filter(m => { |
| const matchesSearch = !searchTerm || |
| m.model_name.toLowerCase().includes(searchTerm) || |
| m.displayVendor.toLowerCase().includes(searchTerm); |
| const matchesQuadrant = !quadrant || m.quadrant === quadrant; |
| return matchesSearch && matchesQuadrant; |
| }); |
|
|
| populateTable(filteredModels); |
| } |
|
|
| searchInput.addEventListener('input', applyFilters); |
| quadrantFilter.addEventListener('change', applyFilters); |
| } |
|
|
| |
| function createAllQuadrantsChart() { |
| const ctx = document.getElementById('allQuadrantsChart').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 |
| }; |
| }); |
|
|
| 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: $${medianCost.toFixed(2)}`, |
| position: 'start' |
| } |
| }, |
| horizontalLine: { |
| type: 'line', |
| yMin: medianContext, |
| yMax: medianContext, |
| borderColor: '#64748b', |
| borderWidth: 2, |
| borderDash: [5, 5], |
| label: { |
| display: true, |
| content: `Median: ${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']] |
| }); |
| } |
|
|
| |
| function createIndividualQuadrantCharts() { |
| const quadrants = [ |
| { name: 'Low Cost / High Context', id: 'lchc', color: '#10b981' }, |
| { name: 'High Cost / High Context', id: 'hchc', color: '#2563eb' }, |
| { name: 'Low Cost / Low Context', id: 'lclc', color: '#f59e0b' }, |
| { name: 'High Cost / Low Context', id: 'hclc', color: '#ef4444' } |
| ]; |
|
|
| quadrants.forEach(quadrant => { |
| const ctx = document.getElementById(`quadrant-${quadrant.id}`).getContext('2d'); |
| const models = allModels.filter(m => m.quadrant === quadrant.name); |
|
|
| if (models.length === 0) return; |
|
|
| const data = models.map(m => ({ |
| x: m.avg_cost, |
| y: m.context_length / 1000, |
| model: m |
| })); |
|
|
| new Chart(ctx, { |
| type: 'scatter', |
| data: { |
| datasets: [{ |
| label: quadrant.name, |
| data: data, |
| backgroundColor: quadrant.color + '80', |
| borderColor: quadrant.color, |
| borderWidth: 2, |
| pointRadius: 6, |
| pointHoverRadius: 9 |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| plugins: { |
| title: { |
| display: false |
| }, |
| legend: { |
| display: false |
| }, |
| 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` |
| ]; |
| } |
| } |
| } |
| }, |
| scales: { |
| x: { |
| type: 'logarithmic', |
| title: { |
| display: true, |
| text: 'Average Cost ($/M tokens, log scale)', |
| font: { |
| size: 11 |
| } |
| } |
| }, |
| y: { |
| title: { |
| display: true, |
| text: 'Context (K tokens)', |
| font: { |
| size: 11 |
| } |
| } |
| } |
| } |
| } |
| }); |
| }); |
| } |
|
|
| |
| document.addEventListener('DOMContentLoaded', loadData); |
|
|