Sign inCreate account
Stats panel embed

Embedding the profile stats panel

Copy-ready code and a visual preview are provided so you can embed without guessing styles.

Sample

Live preview

Use this layout as a reference before pasting to your own page.

Panel contents

Choose the broad stat groups to include in the preview and generated embed code.

Example creator
Top focus
Top 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
Statistics by Draw Stats / Copyright © Draw Stats

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>
Docs

API endpoint

This embed uses `GET /api/public/stats-panel` and the `user_id` and `source_url` pair.