// 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);