| |
|
|
| (async function () { |
|
|
| |
|
|
| function cssVar(name) { |
| return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); |
| } |
|
|
| |
|
|
| const config = await fetch("config.json").then(r => r.json()); |
|
|
| |
|
|
| function parseCSV(text) { |
| const lines = text.replace(/\r/g, "").trim().split("\n"); |
| const headers = lines[0].split(","); |
| |
| const numericCols = new Set( |
| config.metrics.map(m => m.column).concat( |
| config.filters.filter(f => f.type === "number").map(f => f.column), |
| (config.display_columns || []).filter(d => d.type === "number").map(d => d.column) |
| ) |
| ); |
| return lines.slice(1).map(line => { |
| const vals = line.split(","); |
| const row = {}; |
| headers.forEach((h, i) => { |
| const raw = (vals[i] || "").trim(); |
| if (raw === "") { |
| row[h] = numericCols.has(h) ? null : ""; |
| } else if (numericCols.has(h)) { |
| row[h] = raw.toUpperCase() === "OOM" ? null : parseFloat(raw); |
| } else { |
| row[h] = raw; |
| } |
| }); |
| return row; |
| }); |
| } |
|
|
| |
|
|
| const familyDataCache = {}; |
|
|
| async function loadFamilyData(familyKey) { |
| if (familyDataCache[familyKey]) return familyDataCache[familyKey]; |
| const familyCfg = config.model_families?.[familyKey] || {}; |
| const dataFile = familyCfg.data_file; |
| if (!dataFile) return []; |
| const csvText = await fetch(dataFile).then(r => r.text()); |
| const rows = parseCSV(csvText); |
| familyDataCache[familyKey] = rows; |
| return rows; |
| } |
|
|
| |
| let DATA = []; |
|
|
| |
|
|
| const accDataCache = {}; |
|
|
| async function loadAccuracyData(filePath) { |
| if (!filePath) return null; |
| if (accDataCache[filePath]) return accDataCache[filePath]; |
| try { |
| const text = await fetch(filePath).then(r => { |
| if (!r.ok) return null; |
| return r.text(); |
| }); |
| if (!text) return null; |
| const lines = text.replace(/\r/g, "").trim().split("\n"); |
| const headers = lines[0].split(","); |
| const rows = lines.slice(1).map(line => { |
| const vals = line.split(","); |
| const row = {}; |
| headers.forEach((h, i) => { |
| const raw = (vals[i] || "").trim(); |
| row[h] = raw; |
| }); |
| return row; |
| }); |
| const result = { headers, rows }; |
| accDataCache[filePath] = result; |
| return result; |
| } catch { |
| return null; |
| } |
| } |
|
|
| |
|
|
| const MODEL_COL = config.model_column; |
| const FAMILY_COL = config.model_family_column || ""; |
| const LINK_PREFIX = config.model_link_prefix || ""; |
| const OPT_ORG = config.optimized_org || "embedl"; |
| const CHART_CFG = config.chart || {}; |
| const GROUP_BY = CHART_CFG.group_by || config.filters[config.filters.length - 1]?.column || ""; |
|
|
| function isExternalModel(model) { |
| return !model.startsWith(OPT_ORG + "/"); |
| } |
|
|
| |
|
|
| let ALL_MODELS = []; |
| const ALL_FAMILY_KEYS = Object.keys(config.model_families || {}); |
| let MODEL_FAMILIES = {}; |
|
|
| |
| |
| function deriveBaseFamily(key) { |
| const match = key.match(/^(.+?)-(\d+(?:\.\d+)?B)$/i); |
| if (match) return match[1]; |
| return key; |
| } |
|
|
| const BASE_FAMILIES = {}; |
| ALL_FAMILY_KEYS.forEach(key => { |
| const baseName = deriveBaseFamily(key); |
| if (!BASE_FAMILIES[baseName]) { |
| BASE_FAMILIES[baseName] = { configVariants: [], variants: [] }; |
| } |
| BASE_FAMILIES[baseName].configVariants.push(key); |
| BASE_FAMILIES[baseName].variants.push(key); |
| }); |
| const BASE_FAMILY_KEYS = Object.keys(BASE_FAMILIES); |
|
|
| function activeFamilyKey() { |
| if (filters.variant && config.model_families?.[filters.variant]) return filters.variant; |
| const bf = BASE_FAMILIES[filters.baseFamily]; |
| return bf?.configVariants[0] || filters.baseFamily; |
| } |
|
|
| function getActiveModelSet() { |
| if (filters.variant && MODEL_FAMILIES[filters.variant]) { |
| return new Set(MODEL_FAMILIES[filters.variant].models); |
| } |
| return new Set(ALL_MODELS); |
| } |
|
|
| |
| function detectFamilies() { |
| const families = {}; |
|
|
| if (FAMILY_COL) { |
| DATA.forEach(row => { |
| const fk = row[FAMILY_COL]; |
| const model = row[MODEL_COL]; |
| if (!fk) return; |
| if (!families[fk]) families[fk] = { base: fk, models: [] }; |
| if (!families[fk].models.includes(model)) families[fk].models.push(model); |
| }); |
| } else { |
| const externalNames = ALL_MODELS.filter(isExternalModel).map(m => m.split("/").pop()); |
| externalNames.sort((a, b) => b.length - a.length); |
|
|
| ALL_MODELS.forEach(model => { |
| const shortName = model.split("/").pop(); |
| if (isExternalModel(model)) { |
| if (!families[shortName]) families[shortName] = { base: shortName, models: [] }; |
| families[shortName].models.push(model); |
| } else { |
| const match = externalNames.find(base => shortName.startsWith(base)); |
| const key = match || shortName; |
| if (!families[key]) families[key] = { base: key, models: [] }; |
| families[key].models.push(model); |
| } |
| }); |
| } |
|
|
| return families; |
| } |
|
|
| |
|
|
| function hexToRgba(hex, alpha) { |
| const r = parseInt(hex.slice(1,3),16), g = parseInt(hex.slice(3,5),16), b = parseInt(hex.slice(5,7),16); |
| return `rgba(${r},${g},${b},${alpha})`; |
| } |
|
|
| function buildColorPalette() { |
| const barAlpha = 0.75; |
| const neutralAlpha = 0.45; |
| const teal = cssVar("--teal"), green = cssVar("--green"), pink = cssVar("--pink"), |
| purple = cssVar("--purple"), red = cssVar("--red"); |
| return { |
| palette: [ |
| { bg: hexToRgba(teal, barAlpha), border: teal }, |
| { bg: hexToRgba(green, barAlpha), border: green }, |
| { bg: hexToRgba(pink, barAlpha), border: pink }, |
| { bg: hexToRgba(purple, barAlpha), border: purple }, |
| { bg: "rgba(255,209,102," + barAlpha + ")", border: "#ffd166" }, |
| ], |
| neutral: { bg: hexToRgba(cssVar("--neutral"), neutralAlpha), border: cssVar("--neutral") }, |
| }; |
| } |
|
|
| let COLOR_PALETTE, NEUTRAL_COLOR; |
|
|
| const MODEL_COLORS = {}; |
| const MODEL_SHORT = {}; |
|
|
| function assignModelColors() { |
| let colorIdx = 0; |
| const { palette, neutral } = buildColorPalette(); |
| COLOR_PALETTE = palette; NEUTRAL_COLOR = neutral; |
| |
| const currentFamilies = Object.keys(MODEL_FAMILIES); |
| currentFamilies.forEach(fk => { |
| const family = MODEL_FAMILIES[fk]; |
| family.models.forEach(model => { |
| if (isExternalModel(model)) { |
| MODEL_COLORS[model] = NEUTRAL_COLOR; |
| } else { |
| MODEL_COLORS[model] = COLOR_PALETTE[colorIdx % COLOR_PALETTE.length]; |
| colorIdx++; |
| } |
| const name = model.split("/").pop(); |
| const suffix = name.slice(family.base.length).replace(/^-/, ""); |
| MODEL_SHORT[model] = suffix || (isExternalModel(model) ? "Original" : name); |
| }); |
| }); |
| |
| const labelCounts = {}; |
| for (const m of ALL_MODELS) { |
| const lbl = MODEL_SHORT[m]; |
| if (!labelCounts[lbl]) labelCounts[lbl] = []; |
| labelCounts[lbl].push(m); |
| } |
| for (const [lbl, models] of Object.entries(labelCounts)) { |
| if (models.length > 1) { |
| models.forEach(m => { MODEL_SHORT[m] = m.split("/").pop(); }); |
| } |
| } |
| } |
|
|
| |
|
|
| function isOOMRow(row) { |
| return config.metrics.every(m => row[m.column] === null); |
| } |
|
|
| function getActiveRows() { |
| const models = getActiveModelSet(); |
| return DATA.filter(r => models.has(r[MODEL_COL])); |
| } |
|
|
| function availableOptions() { |
| const rows = getActiveRows(); |
| const opts = {}; |
| config.filters.forEach(f => { |
| const vals = [...new Set(rows.map(r => r[f.column]).filter(v => v !== "" && v !== null && v !== undefined))]; |
| if (f.type === "number") vals.sort((a, b) => a - b); |
| opts[f.column] = vals; |
| }); |
| return opts; |
| } |
|
|
| function valueLabel(filterCfg, val) { |
| if (filterCfg.value_labels && filterCfg.value_labels[val]) return filterCfg.value_labels[val]; |
| if (typeof val === "string") return val.charAt(0).toUpperCase() + val.slice(1); |
| return String(val); |
| } |
|
|
| function sortModels(models) { |
| return [...models].sort((a, b) => { |
| const aExt = isExternalModel(a) ? 0 : 1; |
| const bExt = isExternalModel(b) ? 0 : 1; |
| return aExt - bExt || a.localeCompare(b); |
| }); |
| } |
|
|
| |
|
|
| function modelCellHtml(model) { |
| const color = MODEL_COLORS[model]?.border || '#888'; |
| const url = LINK_PREFIX + model; |
| return `<td class="model-cell"><span class="model-dot" style="background:${color}"></span><a href="${url}" target="_blank" rel="noopener" style="color:${color}">${model}</a></td>`; |
| } |
|
|
| function metricCellHtml(val, isBest, extraClass) { |
| const cls = extraClass ? ` class="${extraClass}"` : ' class="metric-cell"'; |
| if (val === null || val === undefined) return `<td${cls}><span class="oom">OOM</span></td>`; |
| const display = typeof val === "number" ? val.toFixed(2) : (val || "β"); |
| const content = isBest ? `<strong style="color: white; opacity: 0.7">${display}</strong>` : display; |
| return `<td${cls}>${content}</td>`; |
| } |
|
|
| |
|
|
| |
| if (config.title) document.getElementById("hero-title").innerHTML = config.title.replace(/^(.*?)(\s\S+)$/, '$1 <span class="accent">$2</span>'); |
| if (config.subtitle) document.getElementById("hero-sub").textContent = config.subtitle; |
|
|
| |
| const familyNav = document.getElementById("family-nav"); |
| function renderSidebar() { |
| let html = ""; |
| BASE_FAMILY_KEYS.forEach(bf => { |
| const isActive = bf === filters.baseFamily; |
| html += `<div class="sidebar-item${isActive ? " active" : ""}" data-base-family="${bf}">${bf}</div>`; |
| if (isActive) { |
| const variants = BASE_FAMILIES[bf].variants; |
| const showVariants = variants.length > 1; |
| if (showVariants) { |
| variants.forEach(v => { |
| const isVariantActive = v === filters.variant; |
| html += `<div class="sidebar-variant${isVariantActive ? " active" : ""}" data-variant="${v}">${v}</div>`; |
| }); |
| } |
| } |
| }); |
| familyNav.innerHTML = html; |
| } |
|
|
| familyNav.addEventListener("click", async e => { |
| const variantItem = e.target.closest(".sidebar-variant"); |
| if (variantItem) { |
| filters.variant = variantItem.dataset.variant; |
| renderSidebar(); |
| updateDependentFilters(); |
| render(); |
| return; |
| } |
| const baseItem = e.target.closest(".sidebar-item"); |
| if (!baseItem) return; |
| const newBase = baseItem.dataset.baseFamily; |
| if (newBase !== filters.baseFamily) { |
| filters.baseFamily = newBase; |
| filters.variant = null; |
| renderSidebar(); |
| await switchBaseFamily(newBase); |
| } else { |
| filters.variant = null; |
| renderSidebar(); |
| updateDependentFilters(); |
| render(); |
| } |
| }); |
|
|
| |
| const filtersBar = document.getElementById("filters-bar"); |
| filtersBar.innerHTML = ""; |
|
|
| config.filters.forEach(f => { |
| filtersBar.appendChild(createFilterGroup(f.label, "filter-" + f.column)); |
| }); |
|
|
| |
| const metricGroup = createFilterGroup("METRIC", "filter-metric"); |
|
|
| function createFilterGroup(label, id) { |
| const div = document.createElement("div"); |
| div.className = "filter-group"; |
| div.innerHTML = `<label>${label}</label><div class="btn-group" id="${id}"></div>`; |
| return div; |
| } |
|
|
| |
| const legendGrid = document.getElementById("legend-grid"); |
| legendGrid.innerHTML = config.metrics.map(m => |
| `<div><strong>${m.short || m.column}</strong> ${m.description || m.label}</div>` |
| ).join(""); |
|
|
| |
|
|
| const filters = { baseFamily: BASE_FAMILY_KEYS[0] || "", variant: null }; |
| config.filters.forEach(f => { filters[f.column] = ""; }); |
| filters.metric = CHART_CFG.default_metric || config.metrics[0]?.column || ""; |
|
|
| |
|
|
| function renderBtnGroup(container, items, activeValue) { |
| container.innerHTML = items.map(({ value, label }) => |
| `<button class="btn${String(value) === String(activeValue) ? " active" : ""}" data-value="${value}">${label}</button>` |
| ).join(""); |
| } |
|
|
| function populateFilters() { |
| renderSidebar(); |
|
|
| |
| const metricEl = metricGroup.querySelector(".btn-group"); |
| renderBtnGroup(metricEl, |
| config.metrics.map(m => ({ value: m.column, label: m.short || m.column })), |
| filters.metric |
| ); |
|
|
| updateDependentFilters(); |
| } |
|
|
| function updateDependentFilters(resetDefaults) { |
| const opts = availableOptions(); |
| const familyCfg = config.model_families?.[activeFamilyKey()] || {}; |
|
|
| config.filters.forEach(f => { |
| let vals = opts[f.column] || []; |
| |
| if (f.value_labels) { |
| const labelOrder = Object.keys(f.value_labels); |
| vals = [...vals].sort((a, b) => { |
| const ai = labelOrder.indexOf(String(a)); |
| const bi = labelOrder.indexOf(String(b)); |
| return (ai === -1 ? Infinity : ai) - (bi === -1 ? Infinity : bi); |
| }); |
| } |
| const strVals = vals.map(String); |
| const needsReset = resetDefaults || !strVals.includes(String(filters[f.column])); |
| if (needsReset) { |
| |
| const defaultVal = f.column === GROUP_BY && familyCfg.default_device; |
| if (defaultVal && strVals.includes(String(defaultVal))) { |
| filters[f.column] = defaultVal; |
| } else { |
| filters[f.column] = vals[0] ?? ""; |
| } |
| } |
|
|
| |
| const items = []; |
| if (f.column === GROUP_BY) { |
| items.push({ value: "all", label: "All" }); |
| } |
| vals.forEach(v => items.push({ value: String(v), label: valueLabel(f, v) })); |
|
|
| const el = document.getElementById("filter-" + f.column); |
| if (el) { |
| renderBtnGroup(el, items, String(filters[f.column])); |
| |
| const effectiveCount = f.column === GROUP_BY ? items.length - 1 : items.length; |
| el.closest(".filter-group").style.display = effectiveCount <= 1 ? "none" : ""; |
| } |
| }); |
| } |
|
|
| |
|
|
| function handleFilterClick(e) { |
| const btn = e.target.closest(".btn"); |
| if (!btn) return; |
| const group = btn.closest(".btn-group"); |
| group.querySelectorAll(".btn").forEach(b => b.classList.remove("active")); |
| btn.classList.add("active"); |
| const key = group.id.replace("filter-", ""); |
| filters[key] = btn.dataset.value; |
| render(); |
| } |
|
|
| metricGroup.addEventListener("click", handleFilterClick); |
| filtersBar.addEventListener("click", handleFilterClick); |
|
|
| |
|
|
| let charts = []; |
|
|
| function buildChart(filtered) { |
| const section = document.getElementById("charts-section"); |
| section.innerHTML = ""; |
| charts.forEach(c => c.destroy()); |
| charts = []; |
|
|
| const familyCfg = config.model_families?.[activeFamilyKey()] || {}; |
| const chartCfg = familyCfg.chart || CHART_CFG; |
| const scenarios = chartCfg.scenarios || []; |
|
|
| const metricCol = filters.metric; |
| const metricCfg = config.metrics.find(m => m.column === metricCol) || {}; |
| const groupFilterCfg = config.filters.find(f => f.column === GROUP_BY); |
|
|
| const groupVal = filters[GROUP_BY]; |
| if (groupVal === "all") return; |
|
|
| const groupLabel = groupFilterCfg?.value_labels?.[groupVal] || String(groupVal); |
| const gRows = filtered.filter(r => String(r[GROUP_BY]) === String(groupVal)); |
| if (!gRows.length) return; |
|
|
| |
| const uniqueModels = new Set(gRows.map(r => r[MODEL_COL])); |
| if (uniqueModels.size <= 1) return; |
|
|
| |
| const scenarioList = scenarios.length |
| ? scenarios |
| : [{ label: "", match: {} }]; |
|
|
| |
| const chartHeader = document.createElement("div"); |
| chartHeader.className = "chart-header"; |
|
|
| const headerLeft = document.createElement("div"); |
| headerLeft.className = "chart-header-left"; |
|
|
| const headerTitle = document.createElement("h3"); |
| headerTitle.className = "chart-heading"; |
| headerTitle.textContent = groupLabel; |
| headerLeft.appendChild(headerTitle); |
|
|
| const headerSubtitle = document.createElement("p"); |
| headerSubtitle.className = "chart-subtitle"; |
| headerLeft.appendChild(headerSubtitle); |
|
|
| chartHeader.appendChild(headerLeft); |
|
|
| if (config.metrics.length > 1) { |
| const metricEl = metricGroup.querySelector(".btn-group"); |
| renderBtnGroup(metricEl, |
| config.metrics.map(m => ({ value: m.column, label: m.short || m.column })), |
| filters.metric |
| ); |
| chartHeader.appendChild(metricGroup); |
| } |
|
|
| section.appendChild(chartHeader); |
|
|
| scenarioList.forEach(scenario => { |
| |
| const matchRows = gRows.filter(r => |
| Object.entries(scenario.match || {}).every(([col, val]) => |
| String(r[col]) === String(val) |
| ) |
| ); |
| |
| |
| const matchedModels = new Set(matchRows.map(r => r[MODEL_COL])); |
| const oomRows = gRows.filter(r => !matchedModels.has(r[MODEL_COL]) && isOOMRow(r) |
| && Object.entries(scenario.match || {}).every(([col, val]) => |
| r[col] === null || r[col] === "" || r[col] === "OOM" || String(r[col]) === String(val) |
| ) |
| ); |
| const allRows = matchRows.concat(oomRows); |
|
|
| const models = sortModels([...new Set(allRows.map(r => r[MODEL_COL]))]); |
| const picked = models.map(m => allRows.find(r => r[MODEL_COL] === m)).filter(Boolean); |
| if (!picked.length) return; |
|
|
| const labels = picked.map(r => MODEL_SHORT[r[MODEL_COL]]); |
| const data = picked.map(r => r[metricCol] === null ? 0 : r[metricCol]); |
| const bgColors = picked.map(r => MODEL_COLORS[r[MODEL_COL]].bg); |
| const borderColors = picked.map(r => MODEL_COLORS[r[MODEL_COL]].border); |
|
|
| const metricHint = metricCfg.higher_is_better ? " (higher is better)" : " (lower is better)"; |
| const yLabel = (metricCfg.label || metricCol) + metricHint; |
|
|
| const chartBlock = document.createElement("div"); |
| chartBlock.className = "chart-block"; |
|
|
| if (scenario.label) { |
| headerSubtitle.textContent = scenario.label; |
| } |
|
|
| const wrap = document.createElement("div"); |
| wrap.className = "chart-wrap"; |
| const canvas = document.createElement("canvas"); |
| wrap.appendChild(canvas); |
| chartBlock.appendChild(wrap); |
| section.appendChild(chartBlock); |
|
|
| const c = new Chart(canvas, { |
| type: "bar", |
| data: { |
| labels, |
| datasets: [{ |
| data, |
| backgroundColor: bgColors, |
| borderColor: borderColors, |
| borderWidth: 2, borderRadius: 6, minBarLength: 4, |
| }], |
| }, |
| options: { |
| responsive: true, maintainAspectRatio: false, |
| plugins: { |
| legend: { display: false }, |
| title: { display: false }, |
| tooltip: { |
| backgroundColor: cssVar("--tooltip-bg"), titleColor: cssVar("--tooltip-text"), bodyColor: cssVar("--tooltip-body"), |
| borderColor: cssVar("--btn-active-border"), borderWidth: 1, |
| callbacks: { |
| label: ctx => { |
| const orig = picked[ctx.dataIndex]?.[metricCol]; |
| return orig === null ? "OOM" : orig.toLocaleString(); |
| }, |
| }, |
| }, |
| }, |
| scales: { |
| y: { beginAtZero: true, title: { display: true, text: yLabel, color: cssVar("--text-muted") }, grid: { color: cssVar("--border") }, ticks: { color: cssVar("--text-dim") } }, |
| x: { grid: { display: false }, ticks: { color: cssVar("--text-muted"), font: { size: 14 } } }, |
| }, |
| }, |
| }); |
| charts.push(c); |
| }); |
| } |
|
|
| |
|
|
| function buildTables(filtered, chartsShown) { |
| const section = document.getElementById("tables-section"); |
| section.innerHTML = ""; |
| const groupFilterCfg = config.filters.find(f => f.column === GROUP_BY); |
| const groupVal = filters[GROUP_BY]; |
| const opts = availableOptions(); |
| let groupVals = groupVal === "all" ? (opts[GROUP_BY] || []) : [groupVal]; |
| if (groupVal === "all" && groupFilterCfg?.value_labels) { |
| const labelOrder = Object.keys(groupFilterCfg.value_labels); |
| groupVals = [...groupVals].sort((a, b) => { |
| const ai = labelOrder.indexOf(String(a)); |
| const bi = labelOrder.indexOf(String(b)); |
| return (ai === -1 ? Infinity : ai) - (bi === -1 ? Infinity : bi); |
| }); |
| } |
|
|
| |
| const visibleDisplay = (config.display_columns || []).filter(dc => { |
| if (!dc.visible_when) return true; |
| return Object.entries(dc.visible_when).every(([filterCol, allowedVals]) => |
| allowedVals.includes(filters[filterCol]) |
| ); |
| }); |
|
|
| |
| const colDefs = [ |
| { key: MODEL_COL, label: "MODEL", isModel: true }, |
| ...visibleDisplay.map(dc => ({ key: dc.column, label: dc.label, description: dc.description || "" })), |
| ...config.metrics.map(m => ({ key: m.column, label: m.short || m.column, isMetric: true, description: m.description || "" })), |
| ]; |
|
|
| |
| const familyCfg = config.model_families?.[activeFamilyKey()] || {}; |
| const sortRules = familyCfg.table_sort || config.table_sort || []; |
| const tableGroupBy = familyCfg.table_group_by || config.table_group_by || ""; |
| const tableGroupCols = Array.isArray(tableGroupBy) ? tableGroupBy : (tableGroupBy ? [tableGroupBy] : []); |
|
|
| groupVals.forEach(gv => { |
| const rows = filtered.filter(r => String(r[GROUP_BY]) === String(gv)); |
| if (!rows.length) return; |
| rows.sort((a, b) => { |
| for (const rule of sortRules) { |
| const col = rule.column; |
| const mul = rule.direction === "desc" ? -1 : 1; |
| if (rule.external_first && col === MODEL_COL) { |
| const aExt = isExternalModel(a[col]) ? 0 : 1; |
| const bExt = isExternalModel(b[col]) ? 0 : 1; |
| if (aExt !== bExt) return (aExt - bExt) * mul; |
| } |
| const av = a[col], bv = b[col]; |
| if (av === bv || (av == null && bv == null)) continue; |
| if (av == null) return 1; |
| if (bv == null) return -1; |
| if (typeof av === "number" && typeof bv === "number") { |
| if (av !== bv) return (av - bv) * mul; |
| } else { |
| const aNum = parseFloat(String(av)); |
| const bNum = parseFloat(String(bv)); |
| if (!isNaN(aNum) && !isNaN(bNum)) { |
| if (aNum !== bNum) return (aNum - bNum) * mul; |
| } |
| const cmp = String(av).localeCompare(String(bv)); |
| if (cmp !== 0) return cmp * mul; |
| } |
| } |
| return 0; |
| }); |
|
|
| |
| let prevGroupVal = undefined; |
|
|
| const card = document.createElement("div"); |
| card.className = "table-card"; |
|
|
| const heading = groupFilterCfg?.value_labels?.[gv] || String(gv); |
|
|
| let html = chartsShown ? '' : `<h3>${heading}</h3>`; |
| html += `<div class="table-scroll"><table><thead><tr>`; |
| const firstMetricIdx = colDefs.findIndex(c => c.isMetric); |
| html += colDefs.map((c, i) => { |
| const tip = c.description ? ` data-tip="${c.description.replace(/"/g, '"')}"` : ''; |
| const cls = i === firstMetricIdx ? ' class="first-metric metric-cell"' : (c.isMetric ? ' class="metric-cell"' : ''); |
| return `<th${tip}${cls}>${c.label}</th>`; |
| }).join(""); |
| html += `</tr></thead><tbody>`; |
| |
| // Compute best metric value per sub-group (tableGroupBy) per column |
| const bestByGroup = {}; |
| const groupRowKey = r => tableGroupCols.length |
| ? tableGroupCols.map(c => String(r[c] ?? "")).join("\0") |
| : "__all__"; |
| const subGroups = tableGroupCols.length |
| ? [...new Set(rows.map(groupRowKey))] |
| : ["__all__"]; |
| subGroups.forEach(sg => { |
| const groupRows = tableGroupCols.length ? rows.filter(r => groupRowKey(r) === sg) : rows; |
| bestByGroup[sg] = {}; |
| colDefs.filter(c => c.isMetric).forEach(c => { |
| const metricCfg = config.metrics.find(m => m.column === c.key); |
| const vals = groupRows.map(r => r[c.key]).filter(v => v !== null && v !== undefined); |
| if (vals.length) { |
| bestByGroup[sg][c.key] = metricCfg?.higher_is_better ? Math.max(...vals) : Math.min(...vals); |
| } |
| }); |
| }); |
| |
| rows.forEach(r => { |
| const oom = isOOMRow(r); |
| let rowClass = ""; |
| if (tableGroupCols.length) { |
| const curVal = groupRowKey(r); |
| if (prevGroupVal !== undefined && curVal !== prevGroupVal) { |
| rowClass = "row-group-break"; |
| } |
| prevGroupVal = curVal; |
| } |
| html += `<tr class="${rowClass}">`; |
| colDefs.forEach((c, i) => { |
| const val = r[c.key]; |
| if (c.isModel) { |
| html += modelCellHtml(val); |
| } else if (c.isMetric || oom) { |
| const cls = i === firstMetricIdx ? "first-metric metric-cell" : "metric-cell"; |
| const sg = groupRowKey(r); |
| const isBest = !oom && val !== null && val !== undefined && val === bestByGroup[sg]?.[c.key]; |
| html += metricCellHtml(oom ? null : val, isBest, cls); |
| } else { |
| html += `<td>${val || "β"}</td>`; |
| } |
| }); |
| html += "</tr>"; |
| }); |
| |
| html += "</tbody></table></div>"; |
| card.innerHTML = html; |
| section.appendChild(card); |
| }); |
| } |
| |
| // βββ Experiment Setup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| |
| function buildExperimentSetup() { |
| const section = document.getElementById("experiment-setup"); |
| section.innerHTML = ""; |
| const familyCfg = config.model_families?.[activeFamilyKey()] || {}; |
| const setupMap = familyCfg.experiment_setup || {}; |
| const groupVal = filters[GROUP_BY]; |
| |
| const deviceVals = groupVal === "all" |
| ? [] |
| : (setupMap[groupVal] ? [groupVal] : []); |
| |
| if (!deviceVals.length) { |
| section.style.display = "none"; |
| return; |
| } |
| section.style.display = ""; |
| |
| deviceVals.forEach(dv => { |
| const text = setupMap[dv]; |
| if (!text) return; |
| const p = document.createElement("p"); |
| p.textContent = text; |
| section.appendChild(p); |
| }); |
| } |
| |
| // βββ Demo Section βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| |
| let currentDemoKey = null; |
| |
| async function buildDemo() { |
| const section = document.getElementById("demo-section"); |
| const familyCfg = config.model_families?.[activeFamilyKey()] || {}; |
| const demoData = familyCfg.demo; |
| const key = activeFamilyKey(); |
| if (!demoData) { |
| section.innerHTML = ""; |
| currentDemoKey = null; |
| return; |
| } |
| // Skip rebuild if demo is already rendered for this family |
| if (key === currentDemoKey) return; |
| currentDemoKey = key; |
| section.innerHTML = ""; |
| if (typeof renderDemo === "function") { |
| await renderDemo(demoData, section, OPT_ORG, familyCfg.data_file, MODEL_COLORS); |
| } |
| } |
| |
| // βββ Accuracy Table βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| |
| async function buildAccuracyTable() { |
| const section = document.getElementById("accuracy-section"); |
| section.innerHTML = ""; |
| const familyCfg = config.model_families?.[activeFamilyKey()] || {}; |
| const accFile = familyCfg.accuracy_file; |
| if (!accFile) return; |
| |
| const accData = await loadAccuracyData(accFile); |
| if (!accData || !accData.rows.length) return; |
| |
| // Filter to active models if a variant is selected |
| const activeModels = getActiveModelSet(); |
| const rows = accData.rows.filter(r => activeModels.has(r[accData.headers[0]])); |
| if (!rows.length) return; |
| |
| const modelCol = accData.headers[0]; |
| const metricCols = accData.headers.slice(1); |
| |
| const card = document.createElement("div"); |
| card.className = "table-card"; |
| |
| // Find best value per column (higher is better for accuracy) |
| const best = {}; |
| metricCols.forEach(col => { |
| const vals = rows.map(r => parseFloat(r[col])).filter(v => !isNaN(v)); |
| if (vals.length) best[col] = Math.max(...vals); |
| }); |
| |
| // Build metric cells for a row |
| function accMetricCells(r) { |
| return metricCols.map(col => { |
| const val = parseFloat(r[col]); |
| const isBest = !isNaN(val) && val === best[col]; |
| return metricCellHtml(isNaN(val) ? r[col] : val, isBest); |
| }).join(""); |
| } |
| |
| const metricHeaders = metricCols.map(h => `<th class="metric-cell">${h}</th>`).join(""); |
| |
| // Fixed model column (desktop) |
| let fixedHtml = `<table><thead><tr><th>MODEL</th></tr></thead><tbody>`; |
| rows.forEach(r => { fixedHtml += `<tr>${modelCellHtml(r[modelCol])}</tr>`; }); |
| fixedHtml += `</tbody></table>`; |
| |
| // Scrollable metric columns (desktop) |
| let scrollHtml = `<table><thead><tr>${metricHeaders}</tr></thead><tbody>`; |
| rows.forEach(r => { scrollHtml += `<tr>${accMetricCells(r)}</tr>`; }); |
| scrollHtml += `</tbody></table>`; |
| |
| // Combined single table (mobile β fully scrollable) |
| let combinedHtml = `<table><thead><tr><th>MODEL</th>${metricHeaders}</tr></thead><tbody>`; |
| rows.forEach(r => { combinedHtml += `<tr>${modelCellHtml(r[modelCol])}${accMetricCells(r)}</tr>`; }); |
| combinedHtml += `</tbody></table>`; |
| |
| const accTitle = familyCfg.accuracy_title || config.accuracy_title || "Accuracy"; |
| const accUrl = familyCfg.accuracy_url; |
| const titleHtml = accUrl |
| ? `<h3><a href="${accUrl}" target="_blank" rel="noopener" class="acc-title-link">${accTitle}</a></h3>` |
| : `<h3>${accTitle}</h3>`; |
| card.innerHTML = `${titleHtml}<div class="table-split"><div class="table-split-fixed">${fixedHtml}</div><div class="table-split-scroll">${scrollHtml}</div></div><div class="table-scroll table-scroll-mobile">${combinedHtml}</div>`; |
| section.appendChild(card); |
| } |
| |
| // βββ Render βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| |
| function render() { |
| const familyModels = getActiveModelSet(); |
| |
| const filtered = DATA.filter(r => { |
| if (!familyModels.has(r[MODEL_COL])) return false; |
| for (const f of config.filters) { |
| const fv = filters[f.column]; |
| if (fv === "all" || fv === "" || fv === undefined) continue; |
| if (String(r[f.column]) !== String(fv)) return false; |
| } |
| return true; |
| }); |
| |
| buildChart(filtered); |
| const chartsShown = charts.length > 0; |
| buildTables(filtered, chartsShown); |
| buildDemo(); |
| buildAccuracyTable(); |
| buildExperimentSetup(); |
| } |
| |
| // βββ Switch Base Family (load data + re-render) βββββββββββββββββββββββββββββββ |
| |
| async function switchBaseFamily(baseFamilyKey) { |
| const bf = BASE_FAMILIES[baseFamilyKey]; |
| if (!bf) return; |
| // Always load from config-defined variants (stable keys with data_file) |
| const loaded = new Set(); |
| let allRows = []; |
| for (const variantKey of bf.configVariants) { |
| const rows = await loadFamilyData(variantKey); |
| const dataFile = config.model_families?.[variantKey]?.data_file; |
| if (dataFile && !loaded.has(dataFile)) { |
| loaded.add(dataFile); |
| allRows = allRows.concat(rows); |
| } else if (!dataFile) { |
| allRows = allRows.concat(rows); |
| } |
| } |
| DATA = allRows; |
| ALL_MODELS = [...new Set(DATA.map(r => r[MODEL_COL]))]; |
| MODEL_FAMILIES = detectFamilies(); |
| // Rebuild display variants from detected model_family values |
| bf.variants = Object.keys(MODEL_FAMILIES).filter(v => |
| deriveBaseFamily(v) === baseFamilyKey |
| ); |
| assignModelColors(); |
| renderSidebar(); |
| updateDependentFilters(true); |
| render(); |
| } |
| |
| // βββ Init βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| |
| populateFilters(); |
| await switchBaseFamily(filters.baseFamily); |
| |
| })(); |
| |