Stats panel embed
Embedding the profile stats panel
Copy-ready code and a visual preview are provided so you can embed without guessing styles.
Live preview
Use this layout as a reference before pasting to your own page.

Example creator
Top focusTop rateTop 11%24 artworks / 21 days
- Stroke volume12,450 strokesAcross 24 artworks
- Average pace593 strokes/dayRecent 21-day average
- Consistency92%Compared to the creator network
Embed code
Copy this code and paste it where you want the Draw Stats panel to appear.
<div id="draw-stats-embed-example-user"></div>
<script type="module">
(async () => {
const container = document.getElementById("draw-stats-embed-example-user");
if (!container) {
return;
}
const config = {"apiEndpoint":"https://drawstats.app/api/public/stats-panel?user_id=example-user","sourceUrl":"https://creator.example/embed/draw-stats","containerId":"draw-stats-embed-example-user","logoUrl":"https://drawstats.app/images/logo.png","iconBaseUrl":"https://drawstats.app/images/metric-icons","sections":["top_rate","stroke_volume","average_pace","consistency"]};
try {
const response = await fetch(config.apiEndpoint, {
headers: {
source_url: config.sourceUrl,
},
});
if (!response.ok) {
throw new Error("HTTP " + String(response.status));
}
const payload = await response.json();
const selectedSections = new Set(Array.isArray(config.sections) ? config.sections : ["top_rate", "stroke_volume", "average_pace", "consistency"]);
const rawSummaryRows = Array.isArray(payload?.panel?.summary_rows)
? payload.panel.summary_rows
: [];
const featuredFocus = Array.isArray(payload?.panel?.featured_focus)
? payload.panel.featured_focus[0]
: null;
const normalizedText = (value) => String(value || "").toLowerCase();
const isOverallRow = (row) => {
const text = normalizedText((row?.id || "") + " " + (row?.label || "") + " " + (row?.value || ""));
return text.includes("overall") || text.includes("総合");
};
const nonOverallRows = rawSummaryRows.filter((row) => row && typeof row === "object" && !isOverallRow(row));
const rowMatchesSection = (row, section) => {
const text = normalizedText((row?.id || "") + " " + (row?.label || "") + " " + (row?.meta || "") + " " + (row?.value || ""));
if (section === "stroke_volume") {
return text.includes("stroke") || text.includes("output") || text.includes("artwork") || text.includes("volume") || text.includes("制作") || text.includes("作品");
}
if (section === "average_pace") {
return text.includes("average") || text.includes("avg") || text.includes("pace") || text.includes("density") || text.includes("per ") || text.includes("/day") || text.includes("平均");
}
if (section === "consistency") {
return text.includes("reliability") || text.includes("stability") || text.includes("consistency") || text.includes("継続") || text.includes("安定");
}
return false;
};
const fallbackRows = nonOverallRows.length > 0 ? nonOverallRows : rawSummaryRows.filter((row) => row && typeof row === "object");
const usedRowKeys = new Set();
const rowForSection = (section, fallbackIndex) => {
const matched = fallbackRows.find((row) => rowMatchesSection(row, section) && !usedRowKeys.has(String(row.label || "") + String(row.value || "")));
const fallback = matched || fallbackRows.find((row, index) => index >= fallbackIndex && !usedRowKeys.has(String(row.label || "") + String(row.value || ""))) || null;
if (fallback) {
usedRowKeys.add(String(fallback.label || "") + String(fallback.value || ""));
}
return fallback;
};
const summaryRows = ["stroke_volume", "average_pace", "consistency"]
.filter((section) => selectedSections.has(section))
.map((section, index) => rowForSection(section, index))
.filter(Boolean);
const focus = selectedSections.has("top_rate") && featuredFocus && typeof featuredFocus === "object"
? featuredFocus
: null;
const iconForRow = (id) => {
const value = String(id || "").toLowerCase();
if (value.includes("artwork") || value.includes("volume")) {
return config.iconBaseUrl + "/artworks.svg";
}
if (value.includes("reliability") || value.includes("stability") || value.includes("consistency")) {
return config.iconBaseUrl + "/reliability.svg";
}
if (value.includes("finish") || value.includes("ready")) {
return config.iconBaseUrl + "/image-readiness.svg";
}
return config.iconBaseUrl + "/public-rate.svg";
};
const progressFromValue = (value) => {
const match = String(value || "").match(/(\d+(?:\.\d+)?)%/);
if (!match) {
return 66;
}
const parsed = Math.max(0, Math.min(100, Number(match[1])));
return String(value || "").toLowerCase().includes("top") ? 100 - parsed : parsed;
};
const initialsFromName = (name) => String(name || "DS")
.trim()
.split(/\s+/)
.map((part) => part[0] || "")
.join("")
.slice(0, 2)
.toUpperCase() || "DS";
const panel = document.createElement("section");
panel.style.cssText = "box-sizing:border-box;max-width:420px;border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:12px;background:#1f1f21;color:#fff;font:14px/1.45 system-ui,-apple-system,Segoe UI,sans-serif;";
const brand = document.createElement("div");
brand.style.cssText = "display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px;";
const logo = document.createElement("img");
logo.src = config.logoUrl;
logo.alt = "Draw Stats";
logo.loading = "lazy";
logo.style.cssText = "display:block;width:88px;height:36px;object-fit:contain;border-radius:6px;background:#151517;";
const creator = document.createElement("div");
creator.style.cssText = "display:grid;grid-template-columns:30px minmax(0,1fr);gap:8px;align-items:center;justify-items:end;min-width:0;";
const avatar = document.createElement("span");
avatar.textContent = initialsFromName(payload?.creator?.display_name);
avatar.style.cssText = "display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:50%;background:#fff;color:#1d1d1f;font-weight:800;font-size:11px;";
const title = document.createElement("h3");
title.style.cssText = "margin:0;max-width:210px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right;font-size:16px;line-height:1.15;";
title.textContent = payload?.creator?.display_name || "Draw Stats";
creator.appendChild(avatar);
creator.appendChild(title);
brand.appendChild(logo);
brand.appendChild(creator);
panel.appendChild(brand);
if (focus && typeof focus === "object") {
const featured = document.createElement("div");
featured.style.cssText = "display:grid;gap:3px;margin-bottom:8px;padding:9px 10px;border:1px solid rgba(255,138,28,.24);border-radius:8px;background:rgba(255,138,28,.1);";
const featuredLabel = document.createElement("span");
featuredLabel.textContent = "Top rate";
featuredLabel.style.cssText = "color:#ffd2aa;font-size:11px;font-weight:700;";
const featuredValue = document.createElement("strong");
const focusAxes = Array.isArray(payload?.panel?.radar_axes) ? payload.panel.radar_axes : [];
const focusAxis = focusAxes.find((axis) => String(axis?.id || "") === String(focus.id || ""));
const focusScore = Number(focusAxis?.value);
const focusTopRate = Number.isFinite(focusScore) ? Math.max(1, 101 - Math.round(focusScore)) : null;
featuredValue.textContent = focusTopRate === null ? (focus.value || focus.grade || "Ready") : "Top " + String(focusTopRate) + "%";
featuredValue.style.cssText = "font-size:20px;line-height:1.05;";
const featuredMeta = document.createElement("small");
featuredMeta.textContent = [focus.value, focus.meta].filter(Boolean).join(" / ");
featuredMeta.style.cssText = "color:rgba(255,255,255,.58);font-size:11px;";
featured.appendChild(featuredLabel);
featured.appendChild(featuredValue);
featured.appendChild(featuredMeta);
panel.appendChild(featured);
}
summaryRows.forEach((row) => {
if (!row || typeof row !== "object") {
return;
}
const line = document.createElement("div");
const icon = document.createElement("img");
const body = document.createElement("div");
const label = document.createElement("span");
const value = document.createElement("strong");
const meta = document.createElement("small");
const progress = document.createElement("span");
const progressFill = document.createElement("span");
line.style.cssText = "display:grid;grid-template-columns:28px minmax(0,1fr);gap:9px;padding:8px 0;border-top:1px solid rgba(255,255,255,.08);";
icon.src = iconForRow(row.id);
icon.alt = "";
icon.loading = "lazy";
icon.style.cssText = "width:28px;height:28px;border-radius:8px;background:rgba(255,138,28,.1);border:1px solid rgba(255,138,28,.22);padding:5px;box-sizing:border-box;";
body.style.cssText = "display:grid;gap:4px;min-width:0;";
label.textContent = row.label || "";
label.style.cssText = "color:rgba(255,255,255,.76);font-size:12px;font-weight:700;";
value.textContent = String(row.value ?? "");
value.style.cssText = "font-size:17px;line-height:1.05;";
meta.textContent = row.meta || "";
meta.style.cssText = "color:rgba(255,255,255,.58);font-size:11px;";
progress.style.cssText = "display:block;height:5px;border-radius:8px;overflow:hidden;background:rgba(255,255,255,.12);";
progressFill.style.cssText = "display:block;height:100%;width:" + progressFromValue(row.value) + "%;border-radius:8px;background:#ff8a1c;";
progress.appendChild(progressFill);
body.appendChild(label);
body.appendChild(value);
body.appendChild(meta);
body.appendChild(progress);
line.appendChild(icon);
line.appendChild(body);
panel.appendChild(line);
});
const attribution = document.createElement("small");
attribution.textContent = payload?.attribution?.text || "Statistics by Draw Stats / Copyright © Draw Stats";
attribution.style.cssText = "display:block;margin-top:8px;color:rgba(255,255,255,.58);font-size:10px;";
panel.appendChild(attribution);
container.replaceChildren();
container.appendChild(panel);
} catch (error) {
container.textContent = "Failed to load Draw Stats embed.";
container.style.cssText = "box-sizing:border-box;max-width:420px;border:1px solid rgba(255,143,143,.24);border-radius:8px;padding:12px;background:#1f1f21;color:#ffc1c1;font:14px/1.45 system-ui,-apple-system,Segoe UI,sans-serif;";
if (typeof console !== "undefined" && typeof console.error === "function") {
console.error("draw-stats embed failed", error);
}
}
})();
</script>API endpoint
This embed uses `GET /api/public/stats-panel` and the `user_id` and `source_url` pair.