| | |
| |
|
| | (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 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 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); |
| | }); |
| | }); |
| | } |
| |
|
| | |
| |
|
| | function isOOMRow(row) { |
| | return config.metrics.every(m => row[m.column] === null); |
| | } |
| |
|
| | function familyRows(familyKey) { |
| | const models = new Set((MODEL_FAMILIES[familyKey] || { models: [] }).models); |
| | return DATA.filter(r => models.has(r[MODEL_COL])); |
| | } |
| |
|
| | function availableOptions(familyKey) { |
| | const rows = familyRows(familyKey); |
| | 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); |
| | }); |
| | } |
| |
|
| | |
| |
|
| | |
| | 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() { |
| | familyNav.innerHTML = ALL_FAMILY_KEYS.map(fk => |
| | `<div class="sidebar-item${fk === filters.family ? " active" : ""}" data-family="${fk}">${fk}</div>` |
| | ).join(""); |
| | } |
| |
|
| | familyNav.addEventListener("click", async e => { |
| | const item = e.target.closest(".sidebar-item"); |
| | if (!item) return; |
| | filters.family = item.dataset.family; |
| | renderSidebar(); |
| | await switchFamily(filters.family); |
| | }); |
| |
|
| | |
| | const filtersBar = document.getElementById("filters-bar"); |
| | filtersBar.innerHTML = ""; |
| |
|
| | config.filters.forEach(f => { |
| | filtersBar.appendChild(createFilterGroup(f.label, "filter-" + f.column)); |
| | }); |
| |
|
| | |
| | filtersBar.appendChild(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 = { family: ALL_FAMILY_KEYS[0] || "" }; |
| | 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 = document.getElementById("filter-metric"); |
| | renderBtnGroup(metricEl, |
| | config.metrics.map(m => ({ value: m.column, label: m.short || m.column })), |
| | filters.metric |
| | ); |
| | metricEl.closest(".filter-group").style.display = (config.metrics.length <= 1 || filters[GROUP_BY] === "all") ? "none" : ""; |
| |
|
| | updateDependentFilters(); |
| | } |
| |
|
| | function updateDependentFilters() { |
| | const opts = availableOptions(filters.family); |
| |
|
| | 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); |
| | if (!strVals.includes(String(filters[f.column]))) { |
| | 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" : ""; |
| | } |
| | }); |
| | } |
| |
|
| | |
| |
|
| | filtersBar.addEventListener("click", 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(); |
| | }); |
| |
|
| | |
| |
|
| | let charts = []; |
| |
|
| | function buildChart(filtered) { |
| | const section = document.getElementById("charts-section"); |
| | section.innerHTML = ""; |
| | charts.forEach(c => c.destroy()); |
| | charts = []; |
| |
|
| | const familyCfg = config.model_families?.[filters.family] || {}; |
| | 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 scenarioList = scenarios.length |
| | ? scenarios |
| | : [{ label: "", match: {} }]; |
| |
|
| | 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"; |
| |
|
| | const heading = document.createElement("h3"); |
| | heading.className = "chart-heading"; |
| | heading.textContent = groupLabel; |
| | chartBlock.appendChild(heading); |
| |
|
| | if (scenario.label) { |
| | const sub = document.createElement("p"); |
| | sub.className = "chart-subtitle"; |
| | sub.textContent = scenario.label; |
| | chartBlock.appendChild(sub); |
| | } |
| |
|
| | 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(filters.family); |
| | const groupVals = groupVal === "all" ? (opts[GROUP_BY] || []) : [groupVal]; |
| |
|
| | |
| | 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?.[filters.family] || {}; |
| | 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"' : ''; |
| | 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]; |
| | const fmCls = i === firstMetricIdx ? ' class="first-metric"' : ''; |
| | if (c.isModel) { |
| | const hfUrl = LINK_PREFIX + val; |
| | const modelColor = MODEL_COLORS[val]?.border || '#888'; |
| | html += `<td class="model-cell"><span class="model-dot" style="background:${modelColor}"></span><a href="${hfUrl}" target="_blank" rel="noopener" style="color:${modelColor}">${val}</a></td>`; |
| | } else if (oom) { |
| | html += `<td${fmCls}><span class="oom">OOM</span></td>`; |
| | } else if (c.isMetric) { |
| | const sg = groupRowKey(r); |
| | const isBest = val !== null && val !== undefined && val === bestByGroup[sg]?.[c.key]; |
| | const display = val === null ? '<span class="oom">OOM</span>' : (typeof val === "number" ? val.toFixed(2) : (val ?? "β")); |
| | const modelColor = MODEL_COLORS[r[MODEL_COL]]?.border || '#888'; |
| | html += `<td${fmCls}>${isBest ? '<strong style="color: white; opacity: 0.7">' + display + '</strong>' : display}</td>`; |
| | } 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?.[filters.family] || {}; |
| | 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); |
| | }); |
| | } |
| | |
| | // βββ Render βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| | |
| | function render() { |
| | const familyModels = MODEL_FAMILIES[filters.family] |
| | ? new Set(MODEL_FAMILIES[filters.family].models) |
| | : new Set(ALL_MODELS); |
| | |
| | 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 = filters[GROUP_BY] !== "all"; |
| | // Toggle metric selector visibility |
| | const metricEl = document.getElementById("filter-metric"); |
| | if (metricEl) { |
| | metricEl.closest(".filter-group").style.display = |
| | (config.metrics.length <= 1 || !chartsShown) ? "none" : ""; |
| | } |
| | buildTables(filtered, chartsShown); |
| | buildExperimentSetup(); |
| | } |
| | |
| | // βββ Switch Family (load data + re-render) ββββββββββββββββββββββββββββββββββββ |
| | |
| | async function switchFamily(familyKey) { |
| | DATA = await loadFamilyData(familyKey); |
| | ALL_MODELS = [...new Set(DATA.map(r => r[MODEL_COL]))]; |
| | MODEL_FAMILIES = detectFamilies(); |
| | assignModelColors(); |
| | updateDependentFilters(); |
| | render(); |
| | } |
| | |
| | // βββ Init βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| | |
| | populateFilters(); |
| | await switchFamily(filters.family); |
| | |
| | })(); |
| | |