Redesign: Table-focused layout with objective data presentation
Browse filesMajor Changes:
- Reorganized into tabbed interface (All Models / Visual Analysis)
- Table is now the centerpiece with color-coded cost and context tiers
- Added sortable columns (click headers to sort)
- Removed subjective "value score" metrics
- Added proper attribution and methodology explanation
Features:
- Color-coded pricing tiers (5 levels from very low to very high)
- Color-coded context window tiers (5 levels from small to ultra)
- Interactive legends explaining tier ranges
- Vendor name normalization (meta-llama → Meta, etc.)
- Separate input/output pricing display
- Tab navigation for better content organization
- Clear quadrant methodology (median-based divisions)
Attribution:
- Data collection by Daniel Rosehill
- Source: OpenRouter API (Nov 8, 2025)
- Notes on pricing variability across providers
- Median cost and context values displayed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- app.js +212 -190
- index.html +87 -61
- style.css +191 -0
|
@@ -2,6 +2,42 @@
|
|
| 2 |
let allModels = [];
|
| 3 |
let filteredModels = [];
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
// Load and process CSV data
|
| 6 |
async function loadData() {
|
| 7 |
try {
|
|
@@ -13,6 +49,10 @@ async function loadData() {
|
|
| 13 |
dynamicTyping: true,
|
| 14 |
complete: function(results) {
|
| 15 |
allModels = results.data.filter(row => row.model_name);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
filteredModels = [...allModels];
|
| 17 |
initializeApp();
|
| 18 |
}
|
|
@@ -22,32 +62,74 @@ async function loadData() {
|
|
| 22 |
}
|
| 23 |
}
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
// Initialize all visualizations and features
|
| 26 |
function initializeApp() {
|
| 27 |
updateStats();
|
| 28 |
-
createQuadrantChart();
|
| 29 |
-
populateTopModels();
|
| 30 |
-
createDistributionCharts();
|
| 31 |
-
generateInsights();
|
| 32 |
populateTable();
|
| 33 |
setupTableControls();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
// Update summary statistics
|
| 37 |
function updateStats() {
|
| 38 |
-
const
|
| 39 |
-
const
|
| 40 |
-
|
| 41 |
-
);
|
| 42 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
document.getElementById('total-models').textContent = allModels.length;
|
| 45 |
-
document.getElementById('
|
| 46 |
-
document.getElementById('
|
| 47 |
document.getElementById('vendors').textContent = vendors;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
}
|
| 49 |
|
| 50 |
-
// Create main quadrant scatter plot
|
| 51 |
function createQuadrantChart() {
|
| 52 |
const ctx = document.getElementById('quadrantChart').getContext('2d');
|
| 53 |
|
|
@@ -64,7 +146,7 @@ function createQuadrantChart() {
|
|
| 64 |
label: quadrant,
|
| 65 |
data: models.map(m => ({
|
| 66 |
x: m.avg_cost,
|
| 67 |
-
y: m.context_length / 1000,
|
| 68 |
model: m
|
| 69 |
})),
|
| 70 |
backgroundColor: quadrantColors[quadrant] + '80',
|
|
@@ -75,6 +157,10 @@ function createQuadrantChart() {
|
|
| 75 |
};
|
| 76 |
});
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
new Chart(ctx, {
|
| 79 |
type: 'scatter',
|
| 80 |
data: { datasets },
|
|
@@ -95,13 +181,44 @@ function createQuadrantChart() {
|
|
| 95 |
const model = context.raw.model;
|
| 96 |
return [
|
| 97 |
model.model_name,
|
| 98 |
-
`Vendor: ${model.
|
| 99 |
`Context: ${(model.context_length / 1000).toFixed(0)}K tokens`,
|
| 100 |
-
`
|
| 101 |
-
`
|
|
|
|
| 102 |
];
|
| 103 |
}
|
| 104 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
}
|
| 106 |
},
|
| 107 |
scales: {
|
|
@@ -120,199 +237,104 @@ function createQuadrantChart() {
|
|
| 120 |
}
|
| 121 |
}
|
| 122 |
}
|
| 123 |
-
}
|
|
|
|
| 124 |
});
|
| 125 |
}
|
| 126 |
|
| 127 |
-
// Populate top value models by quadrant
|
| 128 |
-
function populateTopModels() {
|
| 129 |
-
const quadrants = {
|
| 130 |
-
'Low Cost / High Context': 'lc-hc-models',
|
| 131 |
-
'High Cost / High Context': 'hc-hc-models',
|
| 132 |
-
'Low Cost / Low Context': 'lc-lc-models',
|
| 133 |
-
'High Cost / Low Context': 'hc-lc-models'
|
| 134 |
-
};
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
container.innerHTML = models.map(m => `
|
| 144 |
-
<div class="model-item">
|
| 145 |
-
<div class="model-name">${m.model_name}</div>
|
| 146 |
-
<div class="model-vendor">${m.vendor}</div>
|
| 147 |
-
<div class="model-stats">
|
| 148 |
-
<div class="model-stat">
|
| 149 |
-
<span class="model-stat-label">Context</span>
|
| 150 |
-
<span class="model-stat-value">${(m.context_length / 1000).toFixed(0)}K</span>
|
| 151 |
-
</div>
|
| 152 |
-
<div class="model-stat">
|
| 153 |
-
<span class="model-stat-label">Cost</span>
|
| 154 |
-
<span class="model-stat-value">$${m.avg_cost.toFixed(2)}</span>
|
| 155 |
-
</div>
|
| 156 |
-
<div class="model-stat">
|
| 157 |
-
<span class="model-stat-label">Value</span>
|
| 158 |
-
<span class="model-stat-value">${m.value_score.toFixed(0)}</span>
|
| 159 |
-
</div>
|
| 160 |
-
</div>
|
| 161 |
-
</div>
|
| 162 |
-
`).join('');
|
| 163 |
-
});
|
| 164 |
}
|
| 165 |
|
| 166 |
-
//
|
| 167 |
-
function
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
'256K-512K': 0,
|
| 175 |
-
'512K-1M': 0,
|
| 176 |
-
'>1M': 0
|
| 177 |
-
};
|
| 178 |
-
|
| 179 |
-
allModels.forEach(m => {
|
| 180 |
-
const ctx = m.context_length;
|
| 181 |
-
if (ctx < 64000) contextBins['<64K']++;
|
| 182 |
-
else if (ctx < 128000) contextBins['64K-128K']++;
|
| 183 |
-
else if (ctx < 256000) contextBins['128K-256K']++;
|
| 184 |
-
else if (ctx < 512000) contextBins['256K-512K']++;
|
| 185 |
-
else if (ctx < 1000000) contextBins['512K-1M']++;
|
| 186 |
-
else contextBins['>1M']++;
|
| 187 |
-
});
|
| 188 |
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
labels: Object.keys(contextBins),
|
| 193 |
-
datasets: [{
|
| 194 |
-
label: 'Number of Models',
|
| 195 |
-
data: Object.values(contextBins),
|
| 196 |
-
backgroundColor: '#2563eb'
|
| 197 |
-
}]
|
| 198 |
-
},
|
| 199 |
-
options: {
|
| 200 |
-
responsive: true,
|
| 201 |
-
maintainAspectRatio: true,
|
| 202 |
-
plugins: {
|
| 203 |
-
legend: {
|
| 204 |
-
display: false
|
| 205 |
-
}
|
| 206 |
-
},
|
| 207 |
-
scales: {
|
| 208 |
-
y: {
|
| 209 |
-
beginAtZero: true,
|
| 210 |
-
title: {
|
| 211 |
-
display: true,
|
| 212 |
-
text: 'Count'
|
| 213 |
-
}
|
| 214 |
-
}
|
| 215 |
-
}
|
| 216 |
-
}
|
| 217 |
-
});
|
| 218 |
|
| 219 |
-
//
|
| 220 |
-
const
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
vendorCounts[m.vendor] = (vendorCounts[m.vendor] || 0) + 1;
|
| 224 |
-
});
|
| 225 |
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
new Chart(vendorCtx, {
|
| 231 |
-
type: 'bar',
|
| 232 |
-
data: {
|
| 233 |
-
labels: topVendors.map(v => v[0]),
|
| 234 |
-
datasets: [{
|
| 235 |
-
label: 'Model Count',
|
| 236 |
-
data: topVendors.map(v => v[1]),
|
| 237 |
-
backgroundColor: '#10b981'
|
| 238 |
-
}]
|
| 239 |
-
},
|
| 240 |
-
options: {
|
| 241 |
-
indexAxis: 'y',
|
| 242 |
-
responsive: true,
|
| 243 |
-
maintainAspectRatio: true,
|
| 244 |
-
plugins: {
|
| 245 |
-
legend: {
|
| 246 |
-
display: false
|
| 247 |
-
}
|
| 248 |
-
},
|
| 249 |
-
scales: {
|
| 250 |
-
x: {
|
| 251 |
-
beginAtZero: true,
|
| 252 |
-
title: {
|
| 253 |
-
display: true,
|
| 254 |
-
text: 'Number of Models'
|
| 255 |
-
}
|
| 256 |
-
}
|
| 257 |
-
}
|
| 258 |
}
|
| 259 |
-
});
|
| 260 |
-
}
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
m.context_length > max.context_length ? m : max
|
| 267 |
-
);
|
| 268 |
-
const bestValue = allModels.reduce((max, m) =>
|
| 269 |
-
m.value_score > max.value_score ? m : max
|
| 270 |
-
);
|
| 271 |
-
const avgCost = (allModels.reduce((sum, m) => sum + m.avg_cost, 0) / allModels.length).toFixed(2);
|
| 272 |
-
|
| 273 |
-
const insights = [
|
| 274 |
-
{
|
| 275 |
-
title: 'Free Models Available',
|
| 276 |
-
text: `${freeModels.length} models offer free tool-calling capabilities, with ${freeModels.filter(m => m.context_length > 128000).length} providing >128K context windows.`
|
| 277 |
-
},
|
| 278 |
-
{
|
| 279 |
-
title: 'Longest Context',
|
| 280 |
-
text: `${longestContext.model_name} by ${longestContext.vendor} offers the largest context window at ${(longestContext.context_length / 1000000).toFixed(1)}M tokens.`
|
| 281 |
-
},
|
| 282 |
-
{
|
| 283 |
-
title: 'Best Value',
|
| 284 |
-
text: `${bestValue.model_name} offers the highest value score (${bestValue.value_score.toFixed(0)}), with ${(bestValue.context_length / 1000).toFixed(0)}K context at $${bestValue.avg_cost.toFixed(2)}/M.`
|
| 285 |
-
},
|
| 286 |
-
{
|
| 287 |
-
title: 'Average Pricing',
|
| 288 |
-
text: `The average cost across all models is $${avgCost} per million tokens, with significant variation based on context window size.`
|
| 289 |
}
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
const container = document.getElementById('insights-content');
|
| 293 |
-
container.innerHTML = insights.map(i => `
|
| 294 |
-
<div class="insight-card">
|
| 295 |
-
<div class="insight-title">${i.title}</div>
|
| 296 |
-
<div class="insight-text">${i.text}</div>
|
| 297 |
-
</div>
|
| 298 |
-
`).join('');
|
| 299 |
-
}
|
| 300 |
|
| 301 |
-
|
| 302 |
-
function populateTable(models = filteredModels) {
|
| 303 |
-
const tbody = document.getElementById('table-body');
|
| 304 |
-
tbody.innerHTML = models.map(m => `
|
| 305 |
<tr>
|
| 306 |
<td>${m.model_name}</td>
|
| 307 |
-
<td>${m.
|
| 308 |
-
<td
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
<td><span class="quadrant-badge">${m.quadrant}</span></td>
|
| 311 |
-
<td>${m.value_score.toFixed(0)}</td>
|
| 312 |
</tr>
|
| 313 |
`).join('');
|
| 314 |
}
|
| 315 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
// Setup table filtering and search
|
| 317 |
function setupTableControls() {
|
| 318 |
const searchInput = document.getElementById('search');
|
|
@@ -325,7 +347,7 @@ function setupTableControls() {
|
|
| 325 |
filteredModels = allModels.filter(m => {
|
| 326 |
const matchesSearch = !searchTerm ||
|
| 327 |
m.model_name.toLowerCase().includes(searchTerm) ||
|
| 328 |
-
m.
|
| 329 |
const matchesQuadrant = !quadrant || m.quadrant === quadrant;
|
| 330 |
return matchesSearch && matchesQuadrant;
|
| 331 |
});
|
|
|
|
| 2 |
let allModels = [];
|
| 3 |
let filteredModels = [];
|
| 4 |
|
| 5 |
+
// Vendor name mapping
|
| 6 |
+
const vendorNameMap = {
|
| 7 |
+
'qwen': 'Qwen',
|
| 8 |
+
'meta-llama': 'Meta',
|
| 9 |
+
'x-ai': 'xAI',
|
| 10 |
+
'z-ai': 'Zhipu AI',
|
| 11 |
+
'google': 'Google',
|
| 12 |
+
'openai': 'OpenAI',
|
| 13 |
+
'anthropic': 'Anthropic',
|
| 14 |
+
'mistralai': 'Mistral AI',
|
| 15 |
+
'deepseek': 'DeepSeek',
|
| 16 |
+
'alibaba': 'Alibaba',
|
| 17 |
+
'amazon': 'Amazon',
|
| 18 |
+
'microsoft': 'Microsoft',
|
| 19 |
+
'nvidia': 'NVIDIA',
|
| 20 |
+
'cohere': 'Cohere',
|
| 21 |
+
'ai21': 'AI21 Labs',
|
| 22 |
+
'minimax': 'MiniMax',
|
| 23 |
+
'moonshotai': 'Moonshot AI',
|
| 24 |
+
'stepfun-ai': 'StepFun',
|
| 25 |
+
'inclusionai': 'Inclusion AI',
|
| 26 |
+
'deepcogito': 'Deep Cogito',
|
| 27 |
+
'baidu': 'Baidu',
|
| 28 |
+
'nousresearch': 'Nous Research',
|
| 29 |
+
'arcee-ai': 'Arcee AI',
|
| 30 |
+
'inception': 'Inception',
|
| 31 |
+
'sao10k': 'Sao10K',
|
| 32 |
+
'thedrummer': 'TheDrummer',
|
| 33 |
+
'tngtech': 'TNG Technology',
|
| 34 |
+
'meituan': 'Meituan'
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
function normalizeVendorName(vendor) {
|
| 38 |
+
return vendorNameMap[vendor] || vendor;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
// Load and process CSV data
|
| 42 |
async function loadData() {
|
| 43 |
try {
|
|
|
|
| 49 |
dynamicTyping: true,
|
| 50 |
complete: function(results) {
|
| 51 |
allModels = results.data.filter(row => row.model_name);
|
| 52 |
+
// Normalize vendor names
|
| 53 |
+
allModels.forEach(model => {
|
| 54 |
+
model.displayVendor = normalizeVendorName(model.vendor);
|
| 55 |
+
});
|
| 56 |
filteredModels = [...allModels];
|
| 57 |
initializeApp();
|
| 58 |
}
|
|
|
|
| 62 |
}
|
| 63 |
}
|
| 64 |
|
| 65 |
+
// Sorting state
|
| 66 |
+
let currentSort = {
|
| 67 |
+
column: 'model_name',
|
| 68 |
+
direction: 'asc'
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
// Initialize all visualizations and features
|
| 72 |
function initializeApp() {
|
| 73 |
updateStats();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
populateTable();
|
| 75 |
setupTableControls();
|
| 76 |
+
setupSorting();
|
| 77 |
+
setupTabs();
|
| 78 |
+
createQuadrantChart();
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// Setup tab navigation
|
| 82 |
+
function setupTabs() {
|
| 83 |
+
const tabButtons = document.querySelectorAll('.tab-button');
|
| 84 |
+
const tabContents = document.querySelectorAll('.tab-content');
|
| 85 |
+
|
| 86 |
+
tabButtons.forEach(button => {
|
| 87 |
+
button.addEventListener('click', () => {
|
| 88 |
+
const targetTab = button.dataset.tab;
|
| 89 |
+
|
| 90 |
+
// Remove active class from all buttons and contents
|
| 91 |
+
tabButtons.forEach(btn => btn.classList.remove('active'));
|
| 92 |
+
tabContents.forEach(content => content.classList.remove('active'));
|
| 93 |
+
|
| 94 |
+
// Add active class to clicked button and corresponding content
|
| 95 |
+
button.classList.add('active');
|
| 96 |
+
document.getElementById(`tab-${targetTab}`).classList.add('active');
|
| 97 |
+
});
|
| 98 |
+
});
|
| 99 |
}
|
| 100 |
|
| 101 |
// Update summary statistics
|
| 102 |
function updateStats() {
|
| 103 |
+
const costs = allModels.map(m => m.avg_cost);
|
| 104 |
+
const contexts = allModels.map(m => m.context_length);
|
| 105 |
+
const minCost = Math.min(...costs);
|
| 106 |
+
const maxCost = Math.max(...costs);
|
| 107 |
+
const minContext = Math.min(...contexts);
|
| 108 |
+
const maxContext = Math.max(...contexts);
|
| 109 |
+
const vendors = new Set(allModels.map(m => m.displayVendor)).size;
|
| 110 |
+
|
| 111 |
+
// Calculate medians for methodology display
|
| 112 |
+
const medianCost = median(costs);
|
| 113 |
+
const medianContext = median(contexts);
|
| 114 |
|
| 115 |
document.getElementById('total-models').textContent = allModels.length;
|
| 116 |
+
document.getElementById('price-range').textContent = `$${minCost.toFixed(2)} - $${maxCost.toFixed(2)}`;
|
| 117 |
+
document.getElementById('context-range').textContent = `${(minContext/1000).toFixed(0)}K - ${(maxContext/1000000).toFixed(1)}M`;
|
| 118 |
document.getElementById('vendors').textContent = vendors;
|
| 119 |
+
|
| 120 |
+
// Update methodology section with actual median values
|
| 121 |
+
document.getElementById('median-cost-display').textContent = `$${medianCost.toFixed(2)}/M tokens`;
|
| 122 |
+
document.getElementById('median-context-display').textContent = `${(medianContext/1000).toFixed(0)}K tokens`;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// Calculate median for quadrant lines
|
| 126 |
+
function median(arr) {
|
| 127 |
+
const sorted = [...arr].sort((a, b) => a - b);
|
| 128 |
+
const mid = Math.floor(sorted.length / 2);
|
| 129 |
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
| 130 |
}
|
| 131 |
|
| 132 |
+
// Create main quadrant scatter plot with division lines
|
| 133 |
function createQuadrantChart() {
|
| 134 |
const ctx = document.getElementById('quadrantChart').getContext('2d');
|
| 135 |
|
|
|
|
| 146 |
label: quadrant,
|
| 147 |
data: models.map(m => ({
|
| 148 |
x: m.avg_cost,
|
| 149 |
+
y: m.context_length / 1000,
|
| 150 |
model: m
|
| 151 |
})),
|
| 152 |
backgroundColor: quadrantColors[quadrant] + '80',
|
|
|
|
| 157 |
};
|
| 158 |
});
|
| 159 |
|
| 160 |
+
// Calculate medians for quadrant lines
|
| 161 |
+
const medianCost = median(allModels.map(m => m.avg_cost));
|
| 162 |
+
const medianContext = median(allModels.map(m => m.context_length / 1000));
|
| 163 |
+
|
| 164 |
new Chart(ctx, {
|
| 165 |
type: 'scatter',
|
| 166 |
data: { datasets },
|
|
|
|
| 181 |
const model = context.raw.model;
|
| 182 |
return [
|
| 183 |
model.model_name,
|
| 184 |
+
`Vendor: ${model.displayVendor}`,
|
| 185 |
`Context: ${(model.context_length / 1000).toFixed(0)}K tokens`,
|
| 186 |
+
`Input: $${model.input_price_usd_per_m.toFixed(2)}/M`,
|
| 187 |
+
`Output: $${model.output_price_usd_per_m.toFixed(2)}/M`,
|
| 188 |
+
`Avg: $${model.avg_cost.toFixed(2)}/M`
|
| 189 |
];
|
| 190 |
}
|
| 191 |
}
|
| 192 |
+
},
|
| 193 |
+
annotation: {
|
| 194 |
+
annotations: {
|
| 195 |
+
verticalLine: {
|
| 196 |
+
type: 'line',
|
| 197 |
+
xMin: medianCost,
|
| 198 |
+
xMax: medianCost,
|
| 199 |
+
borderColor: '#64748b',
|
| 200 |
+
borderWidth: 2,
|
| 201 |
+
borderDash: [5, 5],
|
| 202 |
+
label: {
|
| 203 |
+
display: true,
|
| 204 |
+
content: `Median Cost: $${medianCost.toFixed(2)}`,
|
| 205 |
+
position: 'start'
|
| 206 |
+
}
|
| 207 |
+
},
|
| 208 |
+
horizontalLine: {
|
| 209 |
+
type: 'line',
|
| 210 |
+
yMin: medianContext,
|
| 211 |
+
yMax: medianContext,
|
| 212 |
+
borderColor: '#64748b',
|
| 213 |
+
borderWidth: 2,
|
| 214 |
+
borderDash: [5, 5],
|
| 215 |
+
label: {
|
| 216 |
+
display: true,
|
| 217 |
+
content: `Median Context: ${medianContext.toFixed(0)}K`,
|
| 218 |
+
position: 'end'
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
}
|
| 223 |
},
|
| 224 |
scales: {
|
|
|
|
| 237 |
}
|
| 238 |
}
|
| 239 |
}
|
| 240 |
+
},
|
| 241 |
+
plugins: [window['chartjs-plugin-annotation']]
|
| 242 |
});
|
| 243 |
}
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
+
// Get tier class for cost
|
| 247 |
+
function getCostTier(cost) {
|
| 248 |
+
if (cost < 0.10) return 'cost-very-low';
|
| 249 |
+
if (cost < 0.50) return 'cost-low';
|
| 250 |
+
if (cost < 2.00) return 'cost-medium';
|
| 251 |
+
if (cost < 5.00) return 'cost-high';
|
| 252 |
+
return 'cost-very-high';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
}
|
| 254 |
|
| 255 |
+
// Get tier class for context
|
| 256 |
+
function getContextTier(context) {
|
| 257 |
+
if (context < 32000) return 'context-small';
|
| 258 |
+
if (context < 128000) return 'context-medium';
|
| 259 |
+
if (context < 256000) return 'context-large';
|
| 260 |
+
if (context < 1000000) return 'context-very-large';
|
| 261 |
+
return 'context-ultra';
|
| 262 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
|
| 264 |
+
// Populate data table with color coding
|
| 265 |
+
function populateTable(models = filteredModels) {
|
| 266 |
+
const tbody = document.getElementById('table-body');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
+
// Sort models
|
| 269 |
+
const sortedModels = [...models].sort((a, b) => {
|
| 270 |
+
let aVal = a[currentSort.column];
|
| 271 |
+
let bVal = b[currentSort.column];
|
|
|
|
|
|
|
| 272 |
|
| 273 |
+
// Handle string comparisons
|
| 274 |
+
if (typeof aVal === 'string') {
|
| 275 |
+
aVal = aVal.toLowerCase();
|
| 276 |
+
bVal = bVal.toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
}
|
|
|
|
|
|
|
| 278 |
|
| 279 |
+
if (currentSort.direction === 'asc') {
|
| 280 |
+
return aVal > bVal ? 1 : -1;
|
| 281 |
+
} else {
|
| 282 |
+
return aVal < bVal ? 1 : -1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
}
|
| 284 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
+
tbody.innerHTML = sortedModels.map(m => `
|
|
|
|
|
|
|
|
|
|
| 287 |
<tr>
|
| 288 |
<td>${m.model_name}</td>
|
| 289 |
+
<td>${m.displayVendor}</td>
|
| 290 |
+
<td class="${getContextTier(m.context_length)}" style="font-weight: 600;">
|
| 291 |
+
${(m.context_length / 1000).toFixed(0)}K
|
| 292 |
+
</td>
|
| 293 |
+
<td class="${getCostTier(m.input_price_usd_per_m)}" style="font-weight: 600;">
|
| 294 |
+
$${m.input_price_usd_per_m.toFixed(2)}
|
| 295 |
+
</td>
|
| 296 |
+
<td class="${getCostTier(m.output_price_usd_per_m)}" style="font-weight: 600;">
|
| 297 |
+
$${m.output_price_usd_per_m.toFixed(2)}
|
| 298 |
+
</td>
|
| 299 |
<td><span class="quadrant-badge">${m.quadrant}</span></td>
|
|
|
|
| 300 |
</tr>
|
| 301 |
`).join('');
|
| 302 |
}
|
| 303 |
|
| 304 |
+
// Setup sorting functionality
|
| 305 |
+
function setupSorting() {
|
| 306 |
+
const headers = document.querySelectorAll('th.sortable');
|
| 307 |
+
|
| 308 |
+
headers.forEach(header => {
|
| 309 |
+
header.addEventListener('click', () => {
|
| 310 |
+
const column = header.dataset.sort;
|
| 311 |
+
|
| 312 |
+
// Toggle direction if clicking same column
|
| 313 |
+
if (currentSort.column === column) {
|
| 314 |
+
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
| 315 |
+
} else {
|
| 316 |
+
currentSort.column = column;
|
| 317 |
+
currentSort.direction = 'asc';
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
// Update UI indicators
|
| 321 |
+
headers.forEach(h => {
|
| 322 |
+
h.classList.remove('sort-asc', 'sort-desc');
|
| 323 |
+
});
|
| 324 |
+
header.classList.add(`sort-${currentSort.direction}`);
|
| 325 |
+
|
| 326 |
+
// Repopulate table
|
| 327 |
+
populateTable(filteredModels);
|
| 328 |
+
});
|
| 329 |
+
});
|
| 330 |
+
|
| 331 |
+
// Set initial sort indicator
|
| 332 |
+
const initialHeader = document.querySelector(`th[data-sort="${currentSort.column}"]`);
|
| 333 |
+
if (initialHeader) {
|
| 334 |
+
initialHeader.classList.add('sort-asc');
|
| 335 |
+
}
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
// Setup table filtering and search
|
| 339 |
function setupTableControls() {
|
| 340 |
const searchInput = document.getElementById('search');
|
|
|
|
| 347 |
filteredModels = allModels.filter(m => {
|
| 348 |
const matchesSearch = !searchTerm ||
|
| 349 |
m.model_name.toLowerCase().includes(searchTerm) ||
|
| 350 |
+
m.displayVendor.toLowerCase().includes(searchTerm);
|
| 351 |
const matchesQuadrant = !quadrant || m.quadrant === quadrant;
|
| 352 |
return matchesSearch && matchesQuadrant;
|
| 353 |
});
|
|
@@ -6,6 +6,7 @@
|
|
| 6 |
<title>LLM Tool-Calling Models: Cost & Context Analysis</title>
|
| 7 |
<link rel="stylesheet" href="style.css">
|
| 8 |
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
|
|
| 9 |
<script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script>
|
| 10 |
</head>
|
| 11 |
<body>
|
|
@@ -23,6 +24,25 @@
|
|
| 23 |
<li><strong>Cost per token</strong> - Economic efficiency for production use</li>
|
| 24 |
<li><strong>Context window size</strong> - Capability for complex, long-form tasks</li>
|
| 25 |
</ul>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
</section>
|
| 27 |
|
| 28 |
<section class="stats-grid">
|
|
@@ -31,12 +51,12 @@
|
|
| 31 |
<div class="stat-label">Models Analyzed</div>
|
| 32 |
</div>
|
| 33 |
<div class="stat-card">
|
| 34 |
-
<div class="stat-number" id="
|
| 35 |
-
<div class="stat-label">
|
| 36 |
</div>
|
| 37 |
<div class="stat-card">
|
| 38 |
-
<div class="stat-number" id="
|
| 39 |
-
<div class="stat-label">
|
| 40 |
</div>
|
| 41 |
<div class="stat-card">
|
| 42 |
<div class="stat-number" id="vendors">-</div>
|
|
@@ -44,61 +64,47 @@
|
|
| 44 |
</div>
|
| 45 |
</section>
|
| 46 |
|
| 47 |
-
<
|
| 48 |
-
<
|
| 49 |
-
<
|
| 50 |
-
|
| 51 |
-
<canvas id="quadrantChart"></canvas>
|
| 52 |
-
</div>
|
| 53 |
-
</section>
|
| 54 |
|
| 55 |
-
<
|
| 56 |
-
<
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
<
|
| 61 |
-
<
|
| 62 |
-
</
|
| 63 |
-
|
| 64 |
-
<h3>High Cost / High Context</h3>
|
| 65 |
-
<div id="hc-hc-models" class="model-list"></div>
|
| 66 |
-
</div>
|
| 67 |
-
<div class="quadrant-card">
|
| 68 |
-
<h3>Low Cost / Low Context</h3>
|
| 69 |
-
<div id="lc-lc-models" class="model-list"></div>
|
| 70 |
-
</div>
|
| 71 |
-
<div class="quadrant-card">
|
| 72 |
-
<h3>High Cost / Low Context</h3>
|
| 73 |
-
<div id="hc-lc-models" class="model-list"></div>
|
| 74 |
-
</div>
|
| 75 |
-
</div>
|
| 76 |
-
</section>
|
| 77 |
|
| 78 |
-
|
| 79 |
-
<h2>
|
| 80 |
-
<
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
</div>
|
| 85 |
-
<div class="
|
| 86 |
-
<
|
| 87 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
</div>
|
| 89 |
</div>
|
| 90 |
-
</section>
|
| 91 |
-
|
| 92 |
-
<section class="insights">
|
| 93 |
-
<h2>Key Findings</h2>
|
| 94 |
-
<div class="insights-grid" id="insights-content">
|
| 95 |
-
<!-- Populated by JavaScript -->
|
| 96 |
-
</div>
|
| 97 |
-
</section>
|
| 98 |
-
|
| 99 |
-
<section class="data-table-section">
|
| 100 |
-
<h2>All Models</h2>
|
| 101 |
-
<p>Complete dataset with filtering and search</p>
|
| 102 |
<div class="table-controls">
|
| 103 |
<input type="text" id="search" placeholder="Search models..." />
|
| 104 |
<select id="quadrant-filter">
|
|
@@ -113,12 +119,12 @@
|
|
| 113 |
<table id="models-table">
|
| 114 |
<thead>
|
| 115 |
<tr>
|
| 116 |
-
<th>Model Name
|
| 117 |
-
<th>Vendor
|
| 118 |
-
<th>Context
|
| 119 |
-
<th>
|
| 120 |
-
<th>
|
| 121 |
-
<th>
|
| 122 |
</tr>
|
| 123 |
</thead>
|
| 124 |
<tbody id="table-body">
|
|
@@ -126,7 +132,27 @@
|
|
| 126 |
</tbody>
|
| 127 |
</table>
|
| 128 |
</div>
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
</main>
|
| 131 |
|
| 132 |
<footer>
|
|
|
|
| 6 |
<title>LLM Tool-Calling Models: Cost & Context Analysis</title>
|
| 7 |
<link rel="stylesheet" href="style.css">
|
| 8 |
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3.0.1/dist/chartjs-plugin-annotation.min.js"></script>
|
| 10 |
<script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script>
|
| 11 |
</head>
|
| 12 |
<body>
|
|
|
|
| 24 |
<li><strong>Cost per token</strong> - Economic efficiency for production use</li>
|
| 25 |
<li><strong>Context window size</strong> - Capability for complex, long-form tasks</li>
|
| 26 |
</ul>
|
| 27 |
+
|
| 28 |
+
<div class="attribution">
|
| 29 |
+
<h3>Methodology</h3>
|
| 30 |
+
<p><strong>Quadrant Classification:</strong> Models are categorized into four quadrants based on median values calculated from the dataset:</p>
|
| 31 |
+
<ul>
|
| 32 |
+
<li><strong>Cost Division:</strong> Median average cost (mean of input + output pricing) = <span id="median-cost-display">calculated from data</span></li>
|
| 33 |
+
<li><strong>Context Division:</strong> Median context window size = <span id="median-context-display">calculated from data</span></li>
|
| 34 |
+
</ul>
|
| 35 |
+
<p>This creates four categories: Low Cost/High Context, High Cost/High Context, Low Cost/Low Context, and High Cost/Low Context.</p>
|
| 36 |
+
|
| 37 |
+
<h3>Attribution & Data Notes</h3>
|
| 38 |
+
<ul>
|
| 39 |
+
<li><strong>Data Collection:</strong> <a href="https://danielrosehill.com" target="_blank">Daniel Rosehill</a></li>
|
| 40 |
+
<li><strong>Collection Date:</strong> November 8, 2025</li>
|
| 41 |
+
<li><strong>Source:</strong> Prices derived from API calls to <a href="https://openrouter.ai/" target="_blank">OpenRouter</a></li>
|
| 42 |
+
<li><strong>Pricing Variability:</strong> OpenRouter pricing can fluctuate slightly according to the end inference provider. The same models may have slightly different pricing even at the time of capture.</li>
|
| 43 |
+
<li><strong>Data Exclusions:</strong> Free models (cost = $0) were excluded from this analysis</li>
|
| 44 |
+
</ul>
|
| 45 |
+
</div>
|
| 46 |
</section>
|
| 47 |
|
| 48 |
<section class="stats-grid">
|
|
|
|
| 51 |
<div class="stat-label">Models Analyzed</div>
|
| 52 |
</div>
|
| 53 |
<div class="stat-card">
|
| 54 |
+
<div class="stat-number" id="price-range">-</div>
|
| 55 |
+
<div class="stat-label">Price Range ($/M)</div>
|
| 56 |
</div>
|
| 57 |
<div class="stat-card">
|
| 58 |
+
<div class="stat-number" id="context-range">-</div>
|
| 59 |
+
<div class="stat-label">Context Range (K)</div>
|
| 60 |
</div>
|
| 61 |
<div class="stat-card">
|
| 62 |
<div class="stat-number" id="vendors">-</div>
|
|
|
|
| 64 |
</div>
|
| 65 |
</section>
|
| 66 |
|
| 67 |
+
<div class="tabs">
|
| 68 |
+
<button class="tab-button active" data-tab="models">All Models</button>
|
| 69 |
+
<button class="tab-button" data-tab="analysis">Visual Analysis</button>
|
| 70 |
+
</div>
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
+
<div class="tab-content active" id="tab-models">
|
| 73 |
+
<section class="data-notes">
|
| 74 |
+
<h3>Data Notes</h3>
|
| 75 |
+
<ul>
|
| 76 |
+
<li><strong>Cost Calculation:</strong> Input and output pricing shown separately (per million tokens)</li>
|
| 77 |
+
<li><strong>Data Exclusions:</strong> Free models (cost = $0) were excluded from this analysis</li>
|
| 78 |
+
<li><strong>Color Coding:</strong> Cells are color-coded by tier - see legend below for ranges</li>
|
| 79 |
+
</ul>
|
| 80 |
+
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
+
<section class="data-table-section">
|
| 83 |
+
<h2>All Models - Comprehensive View</h2>
|
| 84 |
+
<p>Complete dataset with color-coded cost and context tiers. Click column headers to sort.</p>
|
| 85 |
+
|
| 86 |
+
<div class="tier-legends">
|
| 87 |
+
<div class="legend-group">
|
| 88 |
+
<h4>Cost Tiers ($/M tokens)</h4>
|
| 89 |
+
<div class="legend-items">
|
| 90 |
+
<div class="legend-item"><span class="tier-indicator cost-very-low"></span> Very Low (< $0.10)</div>
|
| 91 |
+
<div class="legend-item"><span class="tier-indicator cost-low"></span> Low ($0.10 - $0.50)</div>
|
| 92 |
+
<div class="legend-item"><span class="tier-indicator cost-medium"></span> Medium ($0.50 - $2.00)</div>
|
| 93 |
+
<div class="legend-item"><span class="tier-indicator cost-high"></span> High ($2.00 - $5.00)</div>
|
| 94 |
+
<div class="legend-item"><span class="tier-indicator cost-very-high"></span> Very High (> $5.00)</div>
|
| 95 |
+
</div>
|
| 96 |
</div>
|
| 97 |
+
<div class="legend-group">
|
| 98 |
+
<h4>Context Window Tiers</h4>
|
| 99 |
+
<div class="legend-items">
|
| 100 |
+
<div class="legend-item"><span class="tier-indicator context-small"></span> Small (< 32K)</div>
|
| 101 |
+
<div class="legend-item"><span class="tier-indicator context-medium"></span> Medium (32K - 128K)</div>
|
| 102 |
+
<div class="legend-item"><span class="tier-indicator context-large"></span> Large (128K - 256K)</div>
|
| 103 |
+
<div class="legend-item"><span class="tier-indicator context-very-large"></span> Very Large (256K - 1M)</div>
|
| 104 |
+
<div class="legend-item"><span class="tier-indicator context-ultra"></span> Ultra (> 1M)</div>
|
| 105 |
+
</div>
|
| 106 |
</div>
|
| 107 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
<div class="table-controls">
|
| 109 |
<input type="text" id="search" placeholder="Search models..." />
|
| 110 |
<select id="quadrant-filter">
|
|
|
|
| 119 |
<table id="models-table">
|
| 120 |
<thead>
|
| 121 |
<tr>
|
| 122 |
+
<th class="sortable" data-sort="model_name">Model Name <span class="sort-indicator"></span></th>
|
| 123 |
+
<th class="sortable" data-sort="displayVendor">Vendor <span class="sort-indicator"></span></th>
|
| 124 |
+
<th class="sortable" data-sort="context_length">Context <span class="sort-indicator"></span></th>
|
| 125 |
+
<th class="sortable" data-sort="input_price_usd_per_m">Input ($/M) <span class="sort-indicator"></span></th>
|
| 126 |
+
<th class="sortable" data-sort="output_price_usd_per_m">Output ($/M) <span class="sort-indicator"></span></th>
|
| 127 |
+
<th class="sortable" data-sort="quadrant">Quadrant <span class="sort-indicator"></span></th>
|
| 128 |
</tr>
|
| 129 |
</thead>
|
| 130 |
<tbody id="table-body">
|
|
|
|
| 132 |
</tbody>
|
| 133 |
</table>
|
| 134 |
</div>
|
| 135 |
+
</section>
|
| 136 |
+
</div>
|
| 137 |
+
|
| 138 |
+
<div class="tab-content" id="tab-analysis">
|
| 139 |
+
<section class="data-notes">
|
| 140 |
+
<h3>Analysis Notes</h3>
|
| 141 |
+
<ul>
|
| 142 |
+
<li><strong>Quadrant Divisions:</strong> Based on median values for cost and context window size</li>
|
| 143 |
+
<li><strong>Logarithmic Scale:</strong> Cost axis uses log scale to better visualize the wide price range</li>
|
| 144 |
+
<li><strong>Median Lines:</strong> Dashed lines show median cost and context values across all models</li>
|
| 145 |
+
</ul>
|
| 146 |
+
</section>
|
| 147 |
+
|
| 148 |
+
<section class="chart-section">
|
| 149 |
+
<h2>Cost vs Context Quadrant Analysis</h2>
|
| 150 |
+
<p>Interactive scatter plot showing the relationship between pricing and context window size. Hover over points for detailed model information.</p>
|
| 151 |
+
<div class="chart-container">
|
| 152 |
+
<canvas id="quadrantChart"></canvas>
|
| 153 |
+
</div>
|
| 154 |
+
</section>
|
| 155 |
+
</div>
|
| 156 |
</main>
|
| 157 |
|
| 158 |
<footer>
|
|
@@ -88,6 +88,94 @@ h3 {
|
|
| 88 |
margin-bottom: 0.5rem;
|
| 89 |
}
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
/* Stats Grid */
|
| 92 |
.stats-grid {
|
| 93 |
display: grid;
|
|
@@ -214,6 +302,32 @@ h3 {
|
|
| 214 |
font-weight: 600;
|
| 215 |
}
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
/* Insights */
|
| 218 |
.insights-grid {
|
| 219 |
display: grid;
|
|
@@ -240,6 +354,58 @@ h3 {
|
|
| 240 |
line-height: 1.5;
|
| 241 |
}
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
/* Table */
|
| 244 |
.table-controls {
|
| 245 |
display: flex;
|
|
@@ -294,6 +460,31 @@ th {
|
|
| 294 |
letter-spacing: 0.05em;
|
| 295 |
}
|
| 296 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
tbody tr {
|
| 298 |
border-bottom: 1px solid var(--border);
|
| 299 |
transition: background-color 0.2s;
|
|
|
|
| 88 |
margin-bottom: 0.5rem;
|
| 89 |
}
|
| 90 |
|
| 91 |
+
.attribution {
|
| 92 |
+
margin-top: 2rem;
|
| 93 |
+
padding: 1.5rem;
|
| 94 |
+
background: var(--bg);
|
| 95 |
+
border-radius: 8px;
|
| 96 |
+
border-left: 4px solid var(--primary);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.attribution h3 {
|
| 100 |
+
color: var(--primary);
|
| 101 |
+
font-size: 1.1rem;
|
| 102 |
+
margin-bottom: 1rem;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.attribution ul {
|
| 106 |
+
margin-left: 1.5rem;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.attribution li {
|
| 110 |
+
margin-bottom: 0.5rem;
|
| 111 |
+
line-height: 1.6;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.attribution a {
|
| 115 |
+
color: var(--primary);
|
| 116 |
+
text-decoration: none;
|
| 117 |
+
font-weight: 600;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.attribution a:hover {
|
| 121 |
+
text-decoration: underline;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
/* Data Notes */
|
| 125 |
+
.data-notes ul {
|
| 126 |
+
margin-left: 1.5rem;
|
| 127 |
+
margin-top: 0.5rem;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.data-notes li {
|
| 131 |
+
margin-bottom: 0.5rem;
|
| 132 |
+
line-height: 1.6;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/* Tabs */
|
| 136 |
+
.tabs {
|
| 137 |
+
display: flex;
|
| 138 |
+
gap: 0.5rem;
|
| 139 |
+
margin: 2rem 0 0 0;
|
| 140 |
+
border-bottom: 2px solid var(--border);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.tab-button {
|
| 144 |
+
padding: 1rem 2rem;
|
| 145 |
+
background: transparent;
|
| 146 |
+
border: none;
|
| 147 |
+
border-bottom: 3px solid transparent;
|
| 148 |
+
color: var(--text-muted);
|
| 149 |
+
font-size: 1rem;
|
| 150 |
+
font-weight: 600;
|
| 151 |
+
cursor: pointer;
|
| 152 |
+
transition: all 0.2s;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.tab-button:hover {
|
| 156 |
+
color: var(--text);
|
| 157 |
+
background: var(--bg);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.tab-button.active {
|
| 161 |
+
color: var(--primary);
|
| 162 |
+
border-bottom-color: var(--primary);
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.tab-content {
|
| 166 |
+
display: none;
|
| 167 |
+
animation: fadeIn 0.3s;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.tab-content.active {
|
| 171 |
+
display: block;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
@keyframes fadeIn {
|
| 175 |
+
from { opacity: 0; }
|
| 176 |
+
to { opacity: 1; }
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
/* Stats Grid */
|
| 180 |
.stats-grid {
|
| 181 |
display: grid;
|
|
|
|
| 302 |
font-weight: 600;
|
| 303 |
}
|
| 304 |
|
| 305 |
+
/* Quadrant Stats */
|
| 306 |
+
.quadrant-stats {
|
| 307 |
+
display: flex;
|
| 308 |
+
flex-direction: column;
|
| 309 |
+
gap: 0.5rem;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.quadrant-stat-item {
|
| 313 |
+
display: flex;
|
| 314 |
+
justify-content: space-between;
|
| 315 |
+
padding: 0.5rem;
|
| 316 |
+
background: var(--card-bg);
|
| 317 |
+
border-radius: 4px;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.quadrant-stat-item .stat-label {
|
| 321 |
+
color: var(--text-muted);
|
| 322 |
+
font-size: 0.85rem;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.quadrant-stat-item .stat-value {
|
| 326 |
+
color: var(--text);
|
| 327 |
+
font-weight: 600;
|
| 328 |
+
font-size: 0.9rem;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
/* Insights */
|
| 332 |
.insights-grid {
|
| 333 |
display: grid;
|
|
|
|
| 354 |
line-height: 1.5;
|
| 355 |
}
|
| 356 |
|
| 357 |
+
/* Tier Legends */
|
| 358 |
+
.tier-legends {
|
| 359 |
+
display: grid;
|
| 360 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 361 |
+
gap: 2rem;
|
| 362 |
+
margin-bottom: 2rem;
|
| 363 |
+
padding: 1.5rem;
|
| 364 |
+
background: var(--bg);
|
| 365 |
+
border-radius: 8px;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.legend-group h4 {
|
| 369 |
+
color: var(--text);
|
| 370 |
+
font-size: 1rem;
|
| 371 |
+
margin-bottom: 1rem;
|
| 372 |
+
font-weight: 600;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
.legend-items {
|
| 376 |
+
display: flex;
|
| 377 |
+
flex-direction: column;
|
| 378 |
+
gap: 0.5rem;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.legend-item {
|
| 382 |
+
display: flex;
|
| 383 |
+
align-items: center;
|
| 384 |
+
gap: 0.75rem;
|
| 385 |
+
font-size: 0.9rem;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.tier-indicator {
|
| 389 |
+
width: 24px;
|
| 390 |
+
height: 24px;
|
| 391 |
+
border-radius: 4px;
|
| 392 |
+
display: inline-block;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
/* Cost Tier Colors */
|
| 396 |
+
.cost-very-low { background: #10b981; }
|
| 397 |
+
.cost-low { background: #84cc16; }
|
| 398 |
+
.cost-medium { background: #f59e0b; }
|
| 399 |
+
.cost-high { background: #f97316; }
|
| 400 |
+
.cost-very-high { background: #ef4444; }
|
| 401 |
+
|
| 402 |
+
/* Context Tier Colors */
|
| 403 |
+
.context-small { background: #fca5a5; }
|
| 404 |
+
.context-medium { background: #fde047; }
|
| 405 |
+
.context-large { background: #a78bfa; }
|
| 406 |
+
.context-very-large { background: #60a5fa; }
|
| 407 |
+
.context-ultra { background: #34d399; }
|
| 408 |
+
|
| 409 |
/* Table */
|
| 410 |
.table-controls {
|
| 411 |
display: flex;
|
|
|
|
| 460 |
letter-spacing: 0.05em;
|
| 461 |
}
|
| 462 |
|
| 463 |
+
th.sortable {
|
| 464 |
+
cursor: pointer;
|
| 465 |
+
user-select: none;
|
| 466 |
+
position: relative;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
th.sortable:hover {
|
| 470 |
+
background: var(--primary-dark);
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
.sort-indicator {
|
| 474 |
+
margin-left: 0.5rem;
|
| 475 |
+
opacity: 0.5;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
th.sort-asc .sort-indicator::after {
|
| 479 |
+
content: ' ▲';
|
| 480 |
+
opacity: 1;
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
th.sort-desc .sort-indicator::after {
|
| 484 |
+
content: ' ▼';
|
| 485 |
+
opacity: 1;
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
tbody tr {
|
| 489 |
border-bottom: 1px solid var(--border);
|
| 490 |
transition: background-color 0.2s;
|