| |
| |
| |
| |
|
|
| |
| function initEmbed(deps) { |
|
|
| const { |
| config, filters, activeFamilyKey, getActiveModelSet, |
| getData, MODEL_COL, FAMILY_COL, GROUP_BY, CHART_CFG, |
| MODEL_COLORS, MODEL_SHORT, isOOMRow, isExternalModel, |
| sortModels, parseModelSize, |
| } = deps; |
|
|
| let embedModal = null; |
| let cachedCommitHash = null; |
|
|
| const HF_DOCS_REPO = "embedl/documentation-images"; |
| const HF_SVG_FOLDER = "Edge-Inference-Benchmarks"; |
| const HF_SPACES_REPO = "embedl/Edge-Inference-Benchmarks"; |
| const HF_SPACES_URL = "https://huggingface.co/spaces/" + HF_SPACES_REPO; |
|
|
| async function loadCommitHash() { |
| if (cachedCommitHash) return cachedCommitHash; |
| try { |
| const resp = await fetch("https://huggingface.co/api/spaces/" + HF_SPACES_REPO); |
| if (resp.ok) { |
| const data = await resp.json(); |
| cachedCommitHash = data.sha ? data.sha.substring(0, 7) : null; |
| } |
| } catch {} |
| return cachedCommitHash; |
| } |
|
|
| function embedFamilyKey() { |
| return filters.variant || activeFamilyKey(); |
| } |
|
|
| function embedFileName() { |
| return embedFamilyKey() + "__" + filters[GROUP_BY] + ".svg"; |
| } |
|
|
| function getEmbedChartData() { |
| 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 null; |
|
|
| const groupLabel = groupFilterCfg?.value_labels?.[groupVal] || String(groupVal); |
| const familyModels = getActiveModelSet(); |
| const filtered = getData().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; |
| }); |
|
|
| const gRows = filtered.filter(r => String(r[GROUP_BY]) === String(groupVal)); |
| if (!gRows.length) return null; |
|
|
| const uniqueModels = new Set(gRows.map(r => r[MODEL_COL])); |
| if (uniqueModels.size <= 1) return null; |
|
|
| const scenarioList = scenarios.length ? scenarios : [{ label: "", match: {} }]; |
|
|
| |
| let scenario = null; |
| let picked = []; |
| for (const sc of scenarioList) { |
| const matchRows = gRows.filter(r => |
| Object.entries(sc.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(sc.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 candidates = models.map(m => allRows.find(r => r[MODEL_COL] === m)).filter(Boolean); |
| if (candidates.length) { |
| scenario = sc; |
| picked = candidates; |
| break; |
| } |
| } |
| if (!picked.length) return null; |
|
|
| const hib = metricCfg.higher_is_better !== false; |
| picked.sort((a, b) => { |
| const sizeA = parseModelSize(a[FAMILY_COL] || a[MODEL_COL]); |
| const sizeB = parseModelSize(b[FAMILY_COL] || b[MODEL_COL]); |
| if (sizeA !== sizeB) return sizeA - sizeB; |
| const extA = isExternalModel(a[MODEL_COL]) ? 0 : 1; |
| const extB = isExternalModel(b[MODEL_COL]) ? 0 : 1; |
| if (extA !== extB) return extA - extB; |
| const va = a[metricCol] ?? 0; |
| const vb = b[metricCol] ?? 0; |
| return hib ? va - vb : vb - va; |
| }); |
|
|
| const rawLabels = picked.map(r => MODEL_SHORT[r[MODEL_COL]]); |
| const families = new Set(picked.map(r => r[FAMILY_COL])); |
| const needPrefix = families.size > 1; |
| const labels = rawLabels.map((lbl, i) => { |
| if (needPrefix) { |
| const fk = picked[i][FAMILY_COL] || ""; |
| return lbl ? `${fk} ${lbl}` : fk; |
| } |
| return lbl; |
| }); |
|
|
| return { |
| familyKey: embedFamilyKey(), |
| groupLabel, |
| scenarioLabel: scenario.label || "", |
| metricCol, |
| metricLabel: metricCfg.short || metricCol, |
| higherIsBetter: hib, |
| picked, |
| labels, |
| }; |
| } |
|
|
| function generateEmbedSVG(version) { |
| const data = getEmbedChartData(); |
| if (!data) return null; |
|
|
| const { familyKey, groupLabel, scenarioLabel, metricCol, metricLabel, higherIsBetter, picked, labels } = data; |
|
|
| const width = 800; |
| const barHeight = 32; |
| const barGap = 10; |
| const topPad = 76; |
| const bottomPad = 44; |
| const leftPad = 260; |
| const rightPad = 90; |
| const barAreaWidth = width - leftPad - rightPad; |
| const contentHeight = picked.length * (barHeight + barGap) - barGap; |
| const height = topPad + contentHeight + bottomPad; |
|
|
| const maxVal = Math.max(...picked.map(r => r[metricCol] ?? 0), 1); |
|
|
| function esc(s) { |
| return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); |
| } |
|
|
| let bars = ""; |
| picked.forEach((r, i) => { |
| const val = r[metricCol] ?? 0; |
| const barW = maxVal > 0 ? Math.max((val / maxVal) * barAreaWidth, 0) : 0; |
| const y = topPad + i * (barHeight + barGap); |
| const cy = y + barHeight / 2 + 5; |
| const color = MODEL_COLORS[r[MODEL_COL]]?.border || "#58b1c3"; |
| const label = labels[i]; |
| const oom = val === 0 && isOOMRow(r); |
| const valText = oom ? "OOM" : val.toFixed(1); |
| const valColor = oom ? "#ff4d6d" : "#e8e8e8"; |
|
|
| bars += ` <text x="${leftPad - 14}" y="${cy}" text-anchor="end" fill="${color}" font-size="13" font-weight="500" font-family="system-ui,-apple-system,sans-serif">${esc(label)}</text>\n`; |
| if (!oom && barW > 0) { |
| bars += ` <rect x="${leftPad}" y="${y}" width="${barW.toFixed(1)}" height="${barHeight}" rx="4" fill="${color}" opacity="0.7"/>\n`; |
| } |
| bars += ` <text x="${leftPad + (oom ? 0 : barW) + 10}" y="${cy}" fill="${valColor}" font-size="13" font-weight="${oom ? "600" : "400"}" font-family="system-ui,-apple-system,sans-serif">${valText}</text>\n`; |
| }); |
|
|
| const hint = higherIsBetter ? "(higher is better)" : "(lower is better)"; |
| const subtitle = [familyKey, groupLabel, scenarioLabel, metricLabel + " " + hint].filter(Boolean).join(" \u00b7 "); |
| const commitAttr = version ? ` data-commit="${esc(version)}"` : ""; |
| return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}"${commitAttr}> |
| <defs> |
| <linearGradient id="topGlow" x1="0" y1="0" x2="1" y2="0"> |
| <stop offset="0%" stop-color="#58b1c3" stop-opacity="0"/> |
| <stop offset="50%" stop-color="#58b1c3" stop-opacity="0.5"/> |
| <stop offset="100%" stop-color="#58b1c3" stop-opacity="0"/> |
| </linearGradient> |
| </defs> |
| <rect width="${width}" height="${height}" rx="12" fill="#0B1527"/> |
| <rect x="0" y="0" width="${width}" height="${height}" rx="12" fill="none" stroke="#58b1c3" stroke-opacity="0.15"/> |
| <rect x="80" y="1" width="${width - 160}" height="2" rx="1" fill="url(#topGlow)"/> |
| <text x="28" y="32" fill="#e8e8e8" font-size="17" font-weight="700" font-family="system-ui,-apple-system,sans-serif">Edge Inference Benchmarks</text> |
| <text x="28" y="54" fill="#8899aa" font-size="12" font-family="system-ui,-apple-system,sans-serif">${esc(subtitle)}</text> |
| ${bars} <line x1="28" y1="${height - 36}" x2="${width - 28}" y2="${height - 36}" stroke="#58b1c3" stroke-opacity="0.15"/> |
| <text x="28" y="${height - 14}" fill="#5a6a7a" font-size="11" font-family="system-ui,-apple-system,sans-serif">embedl.com</text> |
| <text x="${width - 28}" y="${height - 14}" text-anchor="end" fill="#58b1c3" font-size="11" font-family="system-ui,-apple-system,sans-serif">View Benchmarks \u2192</text> |
| </svg>`; |
| } |
|
|
| function createEmbedModal() { |
| const overlay = document.createElement("div"); |
| overlay.className = "embed-overlay"; |
| overlay.innerHTML = ` |
| <div class="embed-modal"> |
| <div class="embed-modal-header"> |
| <h3>Embed in Model Card</h3> |
| <button class="embed-close" aria-label="Close">×</button> |
| </div> |
| <div class="embed-preview" id="embed-preview"></div> |
| <div class="embed-status-bar"> |
| <span class="embed-status" id="embed-status"></span> |
| </div> |
| <div class="embed-actions"> |
| <button class="btn embed-action-btn" id="embed-copy">Copy Embed Code</button> |
| <button class="btn embed-action-btn embed-secondary" id="embed-upload-manual">Download SVG & Open HF</button> |
| </div> |
| <div class="embed-code-section"> |
| <label>Paste this into your HuggingFace model card README.md:</label> |
| <textarea class="embed-code" id="embed-code" readonly rows="6"></textarea> |
| </div> |
| </div> |
| `; |
|
|
| overlay.querySelector(".embed-close").addEventListener("click", () => overlay.classList.remove("visible")); |
| overlay.addEventListener("click", e => { if (e.target === overlay) overlay.classList.remove("visible"); }); |
|
|
| overlay.querySelector("#embed-upload-manual").addEventListener("click", () => { |
| const svg = generateEmbedSVG(cachedCommitHash); |
| if (!svg) return; |
| const fileName = embedFileName(); |
| const blob = new Blob([svg], { type: "image/svg+xml" }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement("a"); |
| a.href = url; |
| a.download = fileName; |
| a.click(); |
| URL.revokeObjectURL(url); |
| const commitMsg = "Update " + fileName + " (" + (cachedCommitHash || "latest") + ")"; |
| window.open("https://huggingface.co/datasets/" + HF_DOCS_REPO + "/upload/main/" + HF_SVG_FOLDER + "?commit_message=" + encodeURIComponent(commitMsg), "_blank"); |
| }); |
|
|
| overlay.querySelector("#embed-copy").addEventListener("click", () => { |
| const textarea = overlay.querySelector("#embed-code"); |
| navigator.clipboard.writeText(textarea.value); |
| const btn = overlay.querySelector("#embed-copy"); |
| const orig = btn.textContent; |
| btn.textContent = "Copied!"; |
| setTimeout(() => { btn.textContent = orig; }, 2000); |
| }); |
|
|
| document.body.appendChild(overlay); |
| return overlay; |
| } |
|
|
| function setEmbedStatus(cls, html) { |
| const el = embedModal.querySelector("#embed-status"); |
| el.className = "embed-status " + cls; |
| el.innerHTML = html; |
| } |
|
|
| function showUploadUI(show) { |
| embedModal.querySelector("#embed-upload-manual").style.display = show ? "" : "none"; |
| } |
|
|
| async function runEmbedUpload() { |
| const version = cachedCommitHash; |
| const fileName = embedFileName(); |
| const svgPath = HF_SVG_FOLDER + "/" + fileName; |
| const svgUrl = "https://huggingface.co/datasets/" + HF_DOCS_REPO + "/resolve/main/" + svgPath; |
|
|
| setEmbedStatus("checking", "Checking\u2026"); |
| showUploadUI(false); |
|
|
| let remoteCommit = null; |
| let exists = false; |
| try { |
| const resp = await fetch(svgUrl, { cache: "no-store" }); |
| if (resp.ok) { |
| exists = true; |
| const text = await resp.text(); |
| const m = text.match(/data-commit="([^"]+)"/); |
| if (m) remoteCommit = m[1]; |
| } |
| } catch {} |
|
|
| if (exists && version && remoteCommit === version) { |
| setEmbedStatus("up-to-date", ""); |
| return; |
| } |
|
|
| if (exists) { |
| setEmbedStatus("needs-token", "SVG outdated \u2014 use Download SVG & Open HF below to update"); |
| } else { |
| setEmbedStatus("needs-token", "SVG not yet uploaded \u2014 use Download SVG & Open HF below to upload manually"); |
| } |
| showUploadUI(true); |
| } |
|
|
| async function showEmbedModal() { |
| if (!embedModal) embedModal = createEmbedModal(); |
|
|
| const version = await loadCommitHash(); |
| const svg = generateEmbedSVG(version); |
| if (!svg) return; |
|
|
| embedModal.querySelector("#embed-preview").innerHTML = svg; |
|
|
| const famKey = embedFamilyKey(); |
| const fileName = embedFileName(); |
| const svgUrl = "https://huggingface.co/datasets/" + HF_DOCS_REPO + "/resolve/main/" + HF_SVG_FOLDER + "/" + fileName; |
|
|
| const embedCode = '<a href="' + HF_SPACES_URL + '" target="_blank" rel="noopener">\n <img\n src="' + svgUrl + '"\n alt="Edge Inference Benchmarks for ' + famKey + '"\n width="100%"\n />\n</a>'; |
|
|
| embedModal.querySelector("#embed-code").value = embedCode; |
| embedModal.classList.add("visible"); |
|
|
| runEmbedUpload(); |
| } |
|
|
| return { showEmbedModal }; |
|
|
| } |
|
|