ai-hardware-analysis / app /src /content /embeds /fig-training-nvidia.html
Franri's picture
feat: adds most of the up-to-date content + figures
2d73c82
Raw
History Blame Contribute Delete
23.3 kB
<div class="nvidia-embed">
<div class="viz-grid">
<div id="title-container"><div class="title-text" id="title-text"></div></div>
<div id="subtitle-container"><div class="subtitle-text" id="subtitle-text"></div></div>
<div id="container1" class="container viz-container">
<div id="viz" role="img" aria-label="Monthly share of NVIDIA, other, and unknown chip providers across top Hugging Face models"></div>
</div>
<div id="container2" class="container legend-container">
<div class="legend" id="legend"></div>
</div>
<div id="container4" class="container logo-container">
<a href="https://huggingface.co/" target="_blank" rel="noopener">
<img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" class="logo logo-hf" alt="Hugging Face logo">
</a>
<span class="logo-collab">×</span>
<a href="https://aiworld.eu/" target="_blank" rel="noopener">
<img src="https://aiworld.eu/logo-transparent.svg" class="logo" alt="AI World logo">
</a>
</div>
</div>
</div>
<style>
.nvidia-embed {
position: relative;
height: clamp(460px, 56vh, 580px);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Ubuntu, Helvetica, Arial, sans-serif;
color: #0f172a;
}
.nvidia-embed .viz-grid {
display: grid;
grid-template-rows: 16fr 1fr 1fr;
grid-template-columns: 1fr 1fr;
height: 100%;
width: 100%;
box-sizing: border-box;
padding: clamp(8px, 2vh, 18px) 0;
background: #ffffff;
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
}
.nvidia-embed #container1 { grid-row: 1; grid-column: 1 / span 2; overflow: hidden; min-height: 0; position: relative; }
.nvidia-embed #title-container { grid-column: 1 / span 2; display: flex; align-items: flex-end; justify-content: flex-start; padding: 0 clamp(12px, 2vw, 24px); overflow: hidden; min-height: 0; }
.nvidia-embed .title-text { font-size: clamp(22px, 4vh, 36px); font-weight: 700; color: #0f172a; line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.nvidia-embed #subtitle-container { grid-column: 1 / span 2; display: flex; align-items: flex-start; justify-content: flex-start; padding: 0 clamp(12px, 2vw, 24px); overflow: hidden; min-height: 0; }
.nvidia-embed .subtitle-text { font-size: clamp(17px, 2.4vw, 24px); font-weight: 500; color: #475569; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.nvidia-embed #container2 { grid-row: 2; grid-column: 1 / span 2; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.nvidia-embed #container4 { grid-row: 3; grid-column: 2; display: flex; align-items: center; justify-content: center; padding: 0 20px; overflow: hidden; min-width: 0; gap: clamp(6px, 1vw, 14px); }
.nvidia-embed #container4 a { height: 100%; display: flex; align-items: center; cursor: pointer; flex-shrink: 0; background: none; border-bottom: none; text-decoration: none; padding: 0; }
.nvidia-embed #container4 a:hover { background: none; border-bottom: none; }
.nvidia-embed .logo-collab { font-size: clamp(12px, 1.6vw, 18px); font-weight: 400; color: #9ca3af; line-height: 1; user-select: none; flex-shrink: 0; }
.nvidia-embed #viz { width: 100%; height: 100%; min-height: 0; min-width: 0; }
.nvidia-embed .legend { display: flex; flex-wrap: nowrap; align-items: center; justify-content: center; gap: clamp(6px, 0.9vw, 12px); background: transparent; padding: 2px 8px; border-radius: 8px; max-width: 100%; font-size: clamp(0.62rem, 0.85vw, 0.8rem); white-space: nowrap; }
.nvidia-embed .legend-item { display: flex; align-items: center; gap: clamp(4px, 0.6vw, 7px); font-weight: 600; color: #333; font-size: inherit; padding: 1px 4px; cursor: default; }
.nvidia-embed .legend-square { width: clamp(8px, 0.8vw, 11px); height: clamp(8px, 0.8vw, 11px); border-radius: 0; flex-shrink: 0; }
.nvidia-embed .logo { height: 100%; width: auto; max-height: 100%; max-width: 100%; pointer-events: auto; object-fit: contain; }
.nvidia-embed .loading-state,
.nvidia-embed .empty-state,
.nvidia-embed .error-state { display: flex; align-items: center; justify-content: center; font-size: clamp(13px, 1.5vw, 16px); color: #475569; width: 100%; height: 100%; text-align: center; }
.nvidia-embed .error-state { color: #b91c1c; }
.nvidia-embed .chart-tooltip { position: absolute; pointer-events: none; background: rgba(255, 255, 255, 0.98); border-radius: 4px; padding: 12px 14px; border: 1px solid #0f172a; min-width: 220px; max-width: 280px; font-size: 12px; color: #0f172a; opacity: 0; transition: opacity 0.15s ease; z-index: 10; }
.nvidia-embed .tooltip-title { font-weight: 600; font-size: 13px; margin-bottom: 6px; }
.nvidia-embed .tooltip-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 12px; color: #0f172a; padding: 2px 0; }
.nvidia-embed .tooltip-row .label { display: flex; align-items: center; gap: 6px; color: #475569; }
.nvidia-embed .tooltip-row .swatch { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; }
.nvidia-embed .tooltip-row .value { font-weight: 700; }
.nvidia-embed .tooltip-total { margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(15, 23, 42, 0.1); font-size: 12px; color: #475569; }
.nvidia-embed .y-grid line { stroke: rgba(148, 163, 184, 0.35); stroke-dasharray: 3 4; }
.nvidia-embed .y-grid .domain { display: none; }
.nvidia-embed .x-axis text,
.nvidia-embed .y-axis text { font-size: 13px; fill: #475569; }
.nvidia-embed .x-axis path,
.nvidia-embed .y-axis path,
.nvidia-embed .x-axis line,
.nvidia-embed .y-axis line { stroke: #d0d7e8; }
.nvidia-embed .focus-line { stroke: rgba(15, 23, 42, 0.7); stroke-width: 1; opacity: 0; pointer-events: none; }
.nvidia-embed .interaction-overlay { cursor: crosshair; }
.nvidia-embed .y-axis-label { font-size: 16px; font-weight: 600; fill: #000000; }
.nvidia-embed .month-axis-label { font-size: 11px; fill: #475569; }
.nvidia-embed .year-axis-label { font-size: 12px; font-weight: 600; fill: #0f172a; }
</style>
<script>
(() => {
function ensureD3(cb) {
if (window.d3 && typeof window.d3.select === "function") return cb();
let s = document.getElementById("d3-cdn-script");
if (!s) {
s = document.createElement("script");
s.id = "d3-cdn-script";
s.src = "https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js";
document.head.appendChild(s);
}
s.addEventListener("load", () => { if (window.d3) cb(); }, { once: true });
if (window.d3 && typeof window.d3.select === "function") cb();
}
function runChart() {
// All DOM lookups are scoped to ROOT: this page hosts a second embed
// that reuses the same element ids (#viz, #container1, …).
const ROOT = document.querySelector(".nvidia-embed");
if (!ROOT || ROOT.dataset.mounted === "true") return;
ROOT.dataset.mounted = "true";
// ── Config (the hosted version read these from the URL; ?title=0) ──────
const BUCKET = "quarter";
const SHOW_TITLES = 0;
const SHOW_CONTAINER_2 = 1;
const SHOW_CONTAINER_3 = 0;
const SHOW_CONTAINER_4 = 1;
(function applyContainerVisibility() {
const vizGrid = ROOT.querySelector(".viz-grid");
const c1 = ROOT.querySelector("#container1");
const c2 = ROOT.querySelector("#container2");
const c3 = ROOT.querySelector("#container3");
const c4 = ROOT.querySelector("#container4");
const title = ROOT.querySelector("#title-container");
const subtitle = ROOT.querySelector("#subtitle-container");
if (!SHOW_TITLES) {
if (title) title.style.display = "none";
if (subtitle) subtitle.style.display = "none";
}
if (!SHOW_CONTAINER_2 && c2) c2.style.display = "none";
if (!SHOW_CONTAINER_3 && c3) c3.style.display = "none";
if (!SHOW_CONTAINER_4 && c4) c4.style.display = "none";
const row3Visible = SHOW_CONTAINER_2;
const row4Visible = SHOW_CONTAINER_3 || SHOW_CONTAINER_4;
let gridRows = SHOW_TITLES ? ["1.6fr", "1fr"] : [];
let currentRow = SHOW_TITLES ? 3 : 1;
const chartRow = currentRow;
const reservedRows = (row3Visible ? 1 : 0) + (row4Visible ? 1 : 0);
const chartHeight = (SHOW_TITLES ? 20 : 18) - reservedRows;
gridRows.push(`${chartHeight}fr`);
if (SHOW_TITLES) {
if (title) title.style.gridRow = "1";
if (subtitle) subtitle.style.gridRow = "2";
}
c1.style.gridRow = String(chartRow);
c1.style.gridColumn = "1 / span 2";
if (row3Visible) { gridRows.push("1fr"); currentRow++; if (c2) c2.style.gridRow = String(currentRow); }
if (row4Visible) {
gridRows.push("1fr"); currentRow++;
if (SHOW_CONTAINER_3 && c3) c3.style.gridRow = String(currentRow);
if (SHOW_CONTAINER_4 && c4) c4.style.gridRow = String(currentRow);
if (row4Visible && SHOW_CONTAINER_3 && !SHOW_CONTAINER_4 && c3) c3.style.gridColumn = "1 / span 2";
if (row4Visible && !SHOW_CONTAINER_3 && SHOW_CONTAINER_4 && c4) c4.style.gridColumn = "1 / span 2";
}
vizGrid.style.gridTemplateRows = gridRows.join(" ");
})();
// ── Copy (titles hidden, but kept so the DOM refs stay valid) ──────────
const CHART_TITLE = "Which chips power top AI models?";
const CHART_SUBTITLE = "Share of NVIDIA, other accelerators, and unknown across top Hugging Face models";
const titleEl = ROOT.querySelector("#title-text");
const subtitleEl = ROOT.querySelector("#subtitle-text");
if (titleEl) titleEl.textContent = CHART_TITLE;
if (subtitleEl) subtitleEl.textContent = CHART_SUBTITLE;
const DATA_URL = "https://huggingface.co/datasets/Franri/model-chips-data/resolve/main/results_top40.json";
const STACK_KEYS = [
{ key: "NVIDIA", label: "NVIDIA", color: "#76b900" },
{ key: "Other", label: "Other", color: "#2756d3" },
{ key: "Unknown", label: "Unknown", color: "#94a3b8" },
];
const COLOR_BY_KEY = Object.fromEntries(STACK_KEYS.map((s) => [s.key, s.color]));
const numberFormatter = d3.format(",");
const percentFormatter = d3.format(".1%");
const vizContainer = ROOT.querySelector("#viz");
const chartContainer = ROOT.querySelector("#container1");
const tooltipElement = document.createElement("div");
tooltipElement.className = "chart-tooltip";
chartContainer.appendChild(tooltipElement);
let preparedRows = [];
let axisYears = [];
function hideTooltip() {
tooltipElement.style.opacity = 0;
}
const bucketStrategies = {
month: {
key: (y, m) => y * 12 + (m - 1),
date: (k) => new Date(Date.UTC(Math.floor(k / 12), k % 12, 15)),
label: (d) => d3.utcFormat("%b")(d),
},
quarter: {
key: (y, m) => y * 4 + Math.floor((m - 1) / 3),
date: (k) => new Date(Date.UTC(Math.floor(k / 4), (k % 4) * 3 + 1, 15)),
label: (d) => `Q${Math.floor(d.getUTCMonth() / 3) + 1}`,
},
year: {
key: (y, m) => y,
date: (k) => new Date(Date.UTC(k, 6, 1)),
label: () => "",
},
};
function aggregate(raw, bucketName) {
const bucket = bucketStrategies[bucketName];
const buckets = new Map();
raw.forEach((d) => {
if (d.year == null || d.month == null) return;
const k = bucket.key(d.year, d.month);
const provider = (d.conclusion && d.conclusion.chip_provider) || "unknown";
let cat;
if (provider === "nvidia") cat = "NVIDIA";
else if (provider === "unknown") cat = "Unknown";
else cat = "Other";
if (!buckets.has(k)) {
buckets.set(k, { NVIDIA: 0, Other: 0, Unknown: 0, Total: 0, date: bucket.date(k) });
}
const row = buckets.get(k);
row[cat]++;
row.Total++;
});
const rows = Array.from(buckets.values()).sort((a, b) => a.date - b.date);
rows.forEach((r) => {
const den = r.Total || 1;
r.NVIDIA_pct = r.NVIDIA / den;
r.Other_pct = r.Other / den;
r.Unknown_pct = r.Unknown / den;
});
return rows;
}
function buildLegend() {
const legend = ROOT.querySelector("#legend");
legend.innerHTML = "";
STACK_KEYS.slice().reverse().forEach((s) => {
const item = document.createElement("div");
item.className = "legend-item";
const square = document.createElement("span");
square.className = "legend-square";
square.style.background = s.color;
const label = document.createElement("span");
label.textContent = s.label;
item.append(square, label);
legend.appendChild(item);
});
}
function renderVisualization(data, width, height) {
if (!data.length || !vizContainer) return;
hideTooltip();
vizContainer.innerHTML = "";
const isSmallWindow = width < 600;
const margin = {
top: isSmallWindow ? 12 : 20,
right: 32,
bottom: isSmallWindow ? 44 : 78,
left: isSmallWindow ? 60 : 90,
};
const innerWidth = Math.max(0, width - margin.left - margin.right);
const innerHeight = Math.max(0, height - margin.top - margin.bottom);
if (!innerWidth || !innerHeight) {
vizContainer.innerHTML = '<div class="empty-state">Not enough space to render the chart.</div>';
return;
}
const yearAxisFontSize = isSmallWindow ? 12 : Math.max(16, Math.min(26, innerWidth * 0.03));
const svg = d3.select(vizContainer).append("svg").attr("width", width).attr("height", height);
const chartGroup = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
const xMin = data[0].date;
const xMax = data[data.length - 1].date;
const x = d3.scaleUtc().domain([xMin, xMax]).range([0, innerWidth]);
const y = d3.scaleLinear().domain([0, 1]).range([innerHeight, 0]);
chartGroup.append("g").attr("class", "y-grid").call(d3.axisLeft(y).ticks(5).tickSize(-innerWidth).tickFormat(""));
const stackKeys = STACK_KEYS.map((s) => `${s.key}_pct`);
const stack = d3.stack().keys(stackKeys);
const series = stack(data);
const area = d3.area()
.x((d) => x(d.data.date))
.y0((d) => y(d[0]))
.y1((d) => y(d[1]))
.curve(d3.curveMonotoneX);
chartGroup.selectAll("path.stacked-area")
.data(series)
.join("path")
.attr("class", "stacked-area")
.attr("fill", (d) => COLOR_BY_KEY[d.key.replace(/_pct$/, "")])
.attr("opacity", 0.92)
.attr("d", area);
axisYears.forEach((year) => {
const yearStart = new Date(Date.UTC(parseInt(year), 0, 1));
if (yearStart > xMin && yearStart < xMax) {
chartGroup.append("line")
.attr("x1", x(yearStart)).attr("x2", x(yearStart))
.attr("y1", 0).attr("y2", innerHeight)
.attr("stroke", "rgba(15, 23, 42, 0.25)")
.attr("stroke-dasharray", "4 4");
}
});
chartGroup.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(x).ticks(0).tickFormat(() => ""));
const subRowY = innerHeight + (isSmallWindow ? 14 : 18);
const yearRowY = innerHeight + (isSmallWindow ? 32 : 50);
const subLabelFn = bucketStrategies[BUCKET].label;
const minLabelGap = BUCKET === "month" ? 26 : 22;
const naturalGap = innerWidth / Math.max(1, data.length - 1);
const subStride = Math.max(1, Math.ceil(minLabelGap / Math.max(1, naturalGap)));
if (BUCKET !== "year") {
chartGroup.append("g")
.attr("class", "sub-axis-labels")
.selectAll("text")
.data(data.filter((_, i) => i % subStride === 0))
.join("text")
.attr("class", "month-axis-label")
.attr("x", (d) => x(d.date))
.attr("y", subRowY)
.attr("text-anchor", "middle")
.text((d) => subLabelFn(d.date));
}
const yearAxisLabelData = axisYears.map((year) => {
const yearStart = new Date(Date.UTC(parseInt(year), 0, 1));
const yearEnd = new Date(Date.UTC(parseInt(year) + 1, 0, 1));
const start = yearStart < xMin ? xMin : yearStart;
const end = yearEnd > xMax ? xMax : yearEnd;
if (start >= end) return null;
const midTime = start.getTime() + (end.getTime() - start.getTime()) / 2;
return { year, x: x(new Date(midTime)) };
}).filter(Boolean);
chartGroup.append("g")
.attr("class", "year-axis-labels")
.selectAll("text")
.data(yearAxisLabelData)
.join("text")
.attr("class", "year-axis-label")
.attr("x", (d) => d.x)
.attr("y", BUCKET === "year" ? subRowY + 2 : yearRowY)
.attr("text-anchor", "middle")
.style("font-size", `${yearAxisFontSize}px`)
.text((d) => d.year);
const yAxis = d3.axisLeft(y)
.ticks(Math.min(5, Math.max(3, Math.floor(innerHeight / 90))))
.tickFormat(d3.format(".0%"));
chartGroup.append("g").attr("class", "y-axis").call(yAxis);
if (!isSmallWindow) {
chartGroup.append("text")
.attr("class", "y-axis-label")
.attr("transform", "rotate(-90)")
.attr("x", -innerHeight / 2)
.attr("y", -margin.left + 24)
.attr("text-anchor", "middle")
.text("Share of models");
}
const focusLine = chartGroup.append("line").attr("class", "focus-line").attr("y1", 0).attr("y2", innerHeight);
const bisectDate = d3.bisector((d) => d.date).left;
const overlay = chartGroup.append("rect")
.attr("class", "interaction-overlay")
.attr("width", innerWidth)
.attr("height", innerHeight)
.attr("fill", "transparent");
const periodLabel = (d) => {
if (BUCKET === "year") return String(d.date.getUTCFullYear());
if (BUCKET === "quarter") return `${d.date.getUTCFullYear()} Q${Math.floor(d.date.getUTCMonth() / 3) + 1}`;
return d3.utcFormat("%b %Y")(d.date);
};
overlay
.on("mousemove", function (event) {
const [mx, my] = d3.pointer(event, this);
if (mx < 0 || mx > innerWidth || my < 0 || my > innerHeight) {
focusLine.style("opacity", 0);
hideTooltip();
return;
}
const hoveredDate = x.invert(mx);
const index = Math.min(Math.max(bisectDate(data, hoveredDate), 1), data.length - 1);
const d0 = data[index - 1];
const d1 = data[index];
const d = hoveredDate - d0.date > d1.date - hoveredDate ? d1 : d0;
const xPos = x(d.date);
focusLine.attr("x1", xPos).attr("x2", xPos).style("opacity", 1);
const rowsHtml = STACK_KEYS.slice().reverse().map((s) => {
const count = d[s.key] || 0;
const share = d[`${s.key}_pct`] || 0;
return `
<div class="tooltip-row">
<span class="label"><span class="swatch" style="background:${s.color}"></span>${s.label}</span>
<span class="value">${percentFormatter(share)} <span style="color:#94a3b8;font-weight:500">(${numberFormatter(count)})</span></span>
</div>`;
}).join("");
tooltipElement.innerHTML = `
<div class="tooltip-title">${periodLabel(d)}</div>
${rowsHtml}
<div class="tooltip-total">${numberFormatter(d.Total)} model${d.Total === 1 ? "" : "s"} in period</div>
`;
tooltipElement.style.opacity = 1;
const tooltipWidth = tooltipElement.offsetWidth || 240;
const tooltipHeight = tooltipElement.offsetHeight || 140;
const containerWidth = vizContainer.clientWidth;
const containerHeight = vizContainer.clientHeight;
let left = margin.left + xPos + 16;
if (left + tooltipWidth > containerWidth) {
left = margin.left + xPos - tooltipWidth - 16;
}
left = Math.max(8, Math.min(containerWidth - tooltipWidth - 8, left));
let top = margin.top + my - tooltipHeight - 12;
if (top < 8) {
top = margin.top + my + 12;
}
top = Math.max(8, Math.min(containerHeight - tooltipHeight - 8, top));
tooltipElement.style.left = `${left}px`;
tooltipElement.style.top = `${top}px`;
})
.on("mouseleave", function () {
focusLine.style("opacity", 0);
hideTooltip();
});
}
function renderVisualizationForContainer() {
if (!vizContainer || !preparedRows.length) return;
const rect = vizContainer.getBoundingClientRect();
renderVisualization(preparedRows, rect.width, rect.height);
}
function loadData() {
if (vizContainer) {
vizContainer.innerHTML = '<div class="loading-state">Loading…</div>';
}
fetch(DATA_URL)
.then((r) => {
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
return r.json();
})
.then((raw) => {
if (!Array.isArray(raw) || !raw.length) {
vizContainer.innerHTML = '<div class="empty-state">No data found.</div>';
return;
}
preparedRows = aggregate(raw, BUCKET);
if (!preparedRows.length) {
vizContainer.innerHTML = '<div class="empty-state">No dated records found.</div>';
return;
}
axisYears = Array.from(new Set(preparedRows.map((d) => d.date.getUTCFullYear()))).sort().map(String);
buildLegend();
renderVisualizationForContainer();
})
.catch((error) => {
console.error(error);
if (vizContainer) {
vizContainer.innerHTML = `<div class="error-state">Unable to load data: ${error.message}</div>`;
}
});
}
let resizeTimer;
window.addEventListener("resize", function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(renderVisualizationForContainer, 220);
});
if (typeof ResizeObserver !== "undefined" && vizContainer) {
new ResizeObserver(renderVisualizationForContainer).observe(vizContainer);
}
loadData();
}
ensureD3(runChart);
})();
</script>