スパークライン

data属性の値からSVGの極小グラフを生成。前回比バッジ付きで、KPIカードやダッシュボードの省スペース指標に向きます。

#svg#sparkline#chart#dashboard

ライブデモ

使用例(お題: SaaS FlowDesk)

この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- FlowDesk:ダッシュボード概要。KPIカードに省スペースのスパークラインを並べる -->
<section class="fs-stage">
  <header class="fs-head">
    <div class="fs-brand"><span class="fs-mark">◆</span> FlowDesk</div>
    <span class="fs-period">直近7日間のサマリー</span>
  </header>

  <!-- 各カードのdata属性をJSが読み取り、SVGスパークラインを生成 -->
  <div class="fs-grid">
    <div class="fs-card" data-label="新規サインアップ" data-unit="件" data-color="#4f7cff"
         data-values="184,201,176,233,258,247,289"></div>
    <div class="fs-card" data-label="MRR" data-unit="万円" data-color="#6fe0a8"
         data-values="412,418,426,431,438,446,459"></div>
    <div class="fs-card" data-label="平均応答時間" data-unit="秒" data-color="#fbbf24"
         data-values="1.8,1.6,1.7,1.5,1.4,1.3,1.2"></div>
    <div class="fs-card" data-label="解約率" data-unit="%" data-color="#fb7185"
         data-values="3.4,3.1,2.9,2.8,2.6,2.5,2.2"></div>
  </div>
</section>
CSS
/* FlowDesk:KPIカード(スパークラインが主役) */
:root {
  --navy: #0f1b34;
  --blue: #4f7cff;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background: radial-gradient(120% 120% at 50% -10%, #1a2c50 0%, #0f1b34 60%, #0a1228 100%);
  color: #eef2ff;
}

.fs-stage { height: 400px; padding: 18px 22px; display: flex; flex-direction: column; }

.fs-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.fs-brand { font-size: 15px; font-weight: 800; letter-spacing: 0.04em; }
.fs-mark { color: var(--blue); }
.fs-period { font-size: 11px; color: rgba(255,255,255,0.55); }

/* KPIカードグリッド */
.fs-grid {
  flex: 1;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}
.fs-card {
  display: flex; flex-direction: column;
  padding: 14px 16px;
  border-radius: 12px;
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.09);
}

/* JSが挿入する行・値・SVG */
.fs-card .row { display: flex; align-items: center; justify-content: space-between; }
.fs-card .name { font-size: 12px; color: #9db4ff; letter-spacing: 0.04em; }
.fs-card .delta { font-size: 11px; font-weight: 700; }
.fs-card .delta.up { color: #6fe0a8; }
.fs-card .delta.down { color: #fb7185; }

.fs-card .value { font-size: 26px; font-weight: 800; }
.fs-card .unit { font-size: 12px; color: rgba(255,255,255,0.6); margin-left: 3px; }

.fs-card svg { width: 100%; height: 40px; margin-top: auto; overflow: visible; }
.spark-line { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }

@media (prefers-reduced-motion: reduce) {
  .spark-line, .spark-dot { transition: none !important; }
}
JavaScript
// FlowDesk:各KPIカードのdata属性から極小SVGスパークラインを生成
(() => {
  const cards = document.querySelectorAll('.fs-card');
  if (!cards.length) return; // null安全
  const NS = 'http://www.w3.org/2000/svg';
  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // viewBox基準サイズ
  const W = 200, H = 40, P = 4;

  cards.forEach((card, ci) => {
    const raw = (card.dataset.values || '').split(',').map(Number).filter((n) => !isNaN(n));
    if (raw.length < 2) return;
    const label = card.dataset.label || '';
    const unit = card.dataset.unit || '';
    const color = card.dataset.color || '#4f7cff';

    const min = Math.min(...raw);
    const max = Math.max(...raw);
    const span = max - min || 1;

    // 値を座標へ写像(上下に少し余白)
    const pts = raw.map((v, i) => ({
      x: P + (i / (raw.length - 1)) * (W - P * 2),
      y: H - P - ((v - min) / span) * (H - P * 2),
    }));
    const dPath = pts.map((p, i) => `${i ? 'L' : 'M'} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(' ');

    // 前回比(最後の2点)。解約率・応答時間は下降が良い
    const last = raw[raw.length - 1];
    const prev = raw[raw.length - 2];
    const diff = last - prev;
    const lowerIsBetter = unit === '%' || unit === '秒';
    const good = lowerIsBetter ? diff < 0 : diff > 0;
    const pct = prev !== 0 ? Math.abs((diff / prev) * 100) : 0;

    // ヘッダ行
    const row = document.createElement('div');
    row.className = 'row';
    const name = document.createElement('span');
    name.className = 'name';
    name.textContent = label;
    const delta = document.createElement('span');
    delta.className = 'delta ' + (good ? 'up' : 'down');
    delta.textContent = `${diff >= 0 ? '▲' : '▼'} ${pct.toFixed(1)}%`;
    row.append(name, delta);

    // 値表示
    const valWrap = document.createElement('div');
    const value = document.createElement('span');
    value.className = 'value';
    value.textContent = Number.isInteger(last) ? last.toLocaleString() : last.toFixed(1);
    const u = document.createElement('span');
    u.className = 'unit';
    u.textContent = unit;
    valWrap.append(value, u);

    // SVGスパークライン
    const svg = document.createElementNS(NS, 'svg');
    svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
    svg.setAttribute('preserveAspectRatio', 'none');
    svg.setAttribute('aria-hidden', 'true');

    const path = document.createElementNS(NS, 'path');
    path.setAttribute('class', 'spark-line');
    path.setAttribute('d', dPath);
    path.setAttribute('stroke', color);
    svg.appendChild(path);

    // 終端のドット
    const dot = document.createElementNS(NS, 'circle');
    dot.setAttribute('class', 'spark-dot');
    dot.setAttribute('cx', pts[pts.length - 1].x);
    dot.setAttribute('cy', pts[pts.length - 1].y);
    dot.setAttribute('r', 3);
    dot.setAttribute('fill', color);
    svg.appendChild(dot);

    card.append(row, valWrap, svg);

    // 描画アニメ
    if (!reduceMotion) {
      const len = path.getTotalLength();
      path.style.strokeDasharray = String(len);
      path.style.strokeDashoffset = String(len);
      dot.style.opacity = '0';
      dot.style.transition = 'opacity .3s ease';
      requestAnimationFrame(() => {
        path.style.transition = 'stroke-dashoffset 1.1s ease';
        path.style.transitionDelay = `${ci * 0.12}s`;
        path.style.strokeDashoffset = '0';
        setTimeout(() => { dot.style.opacity = '1'; }, 1100 + ci * 120);
      });
    }
  });
})();

コード

HTML
<div class="dv-wrap">
  <h2 class="dv-h2">KPI スパークライン</h2>
  <p class="dv-lead">行内に収まる極小グラフ。ダッシュボードのカード指標に最適。</p>
  <!-- 各カードはdata属性で値を持ち、JSがSVGスパークラインを生成する -->
  <div id="cards" class="dv-cards">
    <article class="dv-card" data-label="売上" data-unit="万円" data-color="#22d3ee"
             data-values="120,132,128,145,150,148,162,170,166,182"></article>
    <article class="dv-card" data-label="新規ユーザー" data-unit="人" data-color="#a78bfa"
             data-values="80,76,90,88,102,98,95,110,118,124"></article>
    <article class="dv-card" data-label="解約率" data-unit="%" data-color="#fb7185"
             data-values="4.2,4.0,3.8,3.9,3.5,3.6,3.2,3.0,3.1,2.8"></article>
  </div>
</div>
CSS
:root {
  --dv-ink: #e2e8f0;
  --dv-sub: #94a3b8;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  color: var(--dv-ink);
  background:
    radial-gradient(900px 500px at 50% -20%, #1e293b 0%, transparent 60%),
    linear-gradient(160deg, #0b1220, #020617);
}

.dv-wrap { width: min(92vw, 760px); padding: 22px; }

.dv-h2 { margin: 0; font-size: clamp(18px, 3.4vw, 22px); }
.dv-lead { margin: 4px 0 18px; font-size: 13px; color: var(--dv-sub); }

.dv-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 14px;
}

.dv-card {
  padding: 16px 18px;
  border-radius: 14px;
  background: rgba(15, 23, 42, 0.6);
  border: 1px solid rgba(148, 163, 184, 0.16);
  box-shadow: 0 16px 40px -22px rgba(0, 0, 0, 0.8);
  transition: transform .2s ease, border-color .2s ease;
}
.dv-card:hover {
  transform: translateY(-3px);
  border-color: rgba(148, 163, 184, 0.4);
}

.dv-card .row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 10px;
}
.dv-card .name { font-size: 13px; color: var(--dv-sub); }
.dv-card .delta {
  font-size: 12px;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 999px;
}
.dv-card .delta.up   { color: #34d399; background: rgba(52, 211, 153, 0.12); }
.dv-card .delta.down { color: #fb7185; background: rgba(251, 113, 133, 0.12); }

.dv-card .value {
  font-size: 26px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}
.dv-card .unit { font-size: 12px; color: var(--dv-sub); margin-left: 4px; font-weight: 500; }

.dv-card svg { display: block; width: 100%; height: 44px; margin-top: 10px; }

.dv-card .spark-line {
  fill: none;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.dv-card .spark-dot { stroke: #0b1220; stroke-width: 2; }

@media (prefers-reduced-motion: reduce) {
  .dv-card .spark-line { transition: none !important; }
}
JavaScript
// data属性から値を読み、カードごとに極小SVGスパークラインを生成
(() => {
  const cards = document.querySelectorAll('.dv-card');
  if (!cards.length) return; // null安全
  const NS = 'http://www.w3.org/2000/svg';
  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // viewBox基準のサイズ
  const W = 200, H = 44, P = 4;

  cards.forEach((card, ci) => {
    const raw = (card.dataset.values || '').split(',').map(Number).filter((n) => !isNaN(n));
    if (raw.length < 2) return;
    const label = card.dataset.label || '';
    const unit = card.dataset.unit || '';
    const color = card.dataset.color || '#22d3ee';

    const min = Math.min(...raw);
    const max = Math.max(...raw);
    const span = max - min || 1;

    // 値を座標へ写像(上下に少し余白)
    const pts = raw.map((v, i) => ({
      x: P + (i / (raw.length - 1)) * (W - P * 2),
      y: H - P - ((v - min) / span) * (H - P * 2),
    }));
    const dPath = pts.map((p, i) => `${i ? 'L' : 'M'} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(' ');

    // 前回比(最後の2点)
    const last = raw[raw.length - 1];
    const prev = raw[raw.length - 2];
    const diff = last - prev;
    const isCostMetric = unit === '%'; // 解約率などは下降が良い
    const good = isCostMetric ? diff < 0 : diff > 0;
    const pct = prev !== 0 ? Math.abs((diff / prev) * 100) : 0;

    // ヘッダ行
    const row = document.createElement('div');
    row.className = 'row';
    const name = document.createElement('span');
    name.className = 'name';
    name.textContent = label;
    const delta = document.createElement('span');
    delta.className = 'delta ' + (good ? 'up' : 'down');
    delta.textContent = `${diff >= 0 ? '▲' : '▼'} ${pct.toFixed(1)}%`;
    row.append(name, delta);

    // 値表示
    const valWrap = document.createElement('div');
    const value = document.createElement('span');
    value.className = 'value';
    value.textContent = Number.isInteger(last) ? String(last) : last.toFixed(1);
    const u = document.createElement('span');
    u.className = 'unit';
    u.textContent = unit;
    valWrap.append(value, u);

    // SVGスパークライン
    const svg = document.createElementNS(NS, 'svg');
    svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
    svg.setAttribute('preserveAspectRatio', 'none');
    svg.setAttribute('aria-hidden', 'true');

    const path = document.createElementNS(NS, 'path');
    path.setAttribute('class', 'spark-line');
    path.setAttribute('d', dPath);
    path.setAttribute('stroke', color);
    svg.appendChild(path);

    // 終端のドット
    const dot = document.createElementNS(NS, 'circle');
    dot.setAttribute('class', 'spark-dot');
    dot.setAttribute('cx', pts[pts.length - 1].x);
    dot.setAttribute('cy', pts[pts.length - 1].y);
    dot.setAttribute('r', 3);
    dot.setAttribute('fill', color);
    svg.appendChild(dot);

    card.append(row, valWrap, svg);

    // 描画アニメ
    if (!reduceMotion) {
      const len = path.getTotalLength();
      path.style.strokeDasharray = String(len);
      path.style.strokeDashoffset = String(len);
      dot.style.opacity = '0';
      dot.style.transition = 'opacity .3s ease';
      requestAnimationFrame(() => {
        path.style.transition = 'stroke-dashoffset 1.1s ease';
        path.style.transitionDelay = `${ci * 0.12}s`;
        path.style.strokeDashoffset = '0';
        setTimeout(() => { dot.style.opacity = '1'; }, 1100 + ci * 120);
      });
    }
  });
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「スパークライン」の効果を追加してください。

# 追加してほしい効果
スパークライン(データ可視化)
data属性の値からSVGの極小グラフを生成。前回比バッジ付きで、KPIカードやダッシュボードの省スペース指標に向きます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="dv-wrap">
  <h2 class="dv-h2">KPI スパークライン</h2>
  <p class="dv-lead">行内に収まる極小グラフ。ダッシュボードのカード指標に最適。</p>
  <!-- 各カードはdata属性で値を持ち、JSがSVGスパークラインを生成する -->
  <div id="cards" class="dv-cards">
    <article class="dv-card" data-label="売上" data-unit="万円" data-color="#22d3ee"
             data-values="120,132,128,145,150,148,162,170,166,182"></article>
    <article class="dv-card" data-label="新規ユーザー" data-unit="人" data-color="#a78bfa"
             data-values="80,76,90,88,102,98,95,110,118,124"></article>
    <article class="dv-card" data-label="解約率" data-unit="%" data-color="#fb7185"
             data-values="4.2,4.0,3.8,3.9,3.5,3.6,3.2,3.0,3.1,2.8"></article>
  </div>
</div>

【CSS】
:root {
  --dv-ink: #e2e8f0;
  --dv-sub: #94a3b8;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  color: var(--dv-ink);
  background:
    radial-gradient(900px 500px at 50% -20%, #1e293b 0%, transparent 60%),
    linear-gradient(160deg, #0b1220, #020617);
}

.dv-wrap { width: min(92vw, 760px); padding: 22px; }

.dv-h2 { margin: 0; font-size: clamp(18px, 3.4vw, 22px); }
.dv-lead { margin: 4px 0 18px; font-size: 13px; color: var(--dv-sub); }

.dv-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 14px;
}

.dv-card {
  padding: 16px 18px;
  border-radius: 14px;
  background: rgba(15, 23, 42, 0.6);
  border: 1px solid rgba(148, 163, 184, 0.16);
  box-shadow: 0 16px 40px -22px rgba(0, 0, 0, 0.8);
  transition: transform .2s ease, border-color .2s ease;
}
.dv-card:hover {
  transform: translateY(-3px);
  border-color: rgba(148, 163, 184, 0.4);
}

.dv-card .row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 10px;
}
.dv-card .name { font-size: 13px; color: var(--dv-sub); }
.dv-card .delta {
  font-size: 12px;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 999px;
}
.dv-card .delta.up   { color: #34d399; background: rgba(52, 211, 153, 0.12); }
.dv-card .delta.down { color: #fb7185; background: rgba(251, 113, 133, 0.12); }

.dv-card .value {
  font-size: 26px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}
.dv-card .unit { font-size: 12px; color: var(--dv-sub); margin-left: 4px; font-weight: 500; }

.dv-card svg { display: block; width: 100%; height: 44px; margin-top: 10px; }

.dv-card .spark-line {
  fill: none;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.dv-card .spark-dot { stroke: #0b1220; stroke-width: 2; }

@media (prefers-reduced-motion: reduce) {
  .dv-card .spark-line { transition: none !important; }
}

【JavaScript】
// data属性から値を読み、カードごとに極小SVGスパークラインを生成
(() => {
  const cards = document.querySelectorAll('.dv-card');
  if (!cards.length) return; // null安全
  const NS = 'http://www.w3.org/2000/svg';
  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // viewBox基準のサイズ
  const W = 200, H = 44, P = 4;

  cards.forEach((card, ci) => {
    const raw = (card.dataset.values || '').split(',').map(Number).filter((n) => !isNaN(n));
    if (raw.length < 2) return;
    const label = card.dataset.label || '';
    const unit = card.dataset.unit || '';
    const color = card.dataset.color || '#22d3ee';

    const min = Math.min(...raw);
    const max = Math.max(...raw);
    const span = max - min || 1;

    // 値を座標へ写像(上下に少し余白)
    const pts = raw.map((v, i) => ({
      x: P + (i / (raw.length - 1)) * (W - P * 2),
      y: H - P - ((v - min) / span) * (H - P * 2),
    }));
    const dPath = pts.map((p, i) => `${i ? 'L' : 'M'} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(' ');

    // 前回比(最後の2点)
    const last = raw[raw.length - 1];
    const prev = raw[raw.length - 2];
    const diff = last - prev;
    const isCostMetric = unit === '%'; // 解約率などは下降が良い
    const good = isCostMetric ? diff < 0 : diff > 0;
    const pct = prev !== 0 ? Math.abs((diff / prev) * 100) : 0;

    // ヘッダ行
    const row = document.createElement('div');
    row.className = 'row';
    const name = document.createElement('span');
    name.className = 'name';
    name.textContent = label;
    const delta = document.createElement('span');
    delta.className = 'delta ' + (good ? 'up' : 'down');
    delta.textContent = `${diff >= 0 ? '▲' : '▼'} ${pct.toFixed(1)}%`;
    row.append(name, delta);

    // 値表示
    const valWrap = document.createElement('div');
    const value = document.createElement('span');
    value.className = 'value';
    value.textContent = Number.isInteger(last) ? String(last) : last.toFixed(1);
    const u = document.createElement('span');
    u.className = 'unit';
    u.textContent = unit;
    valWrap.append(value, u);

    // SVGスパークライン
    const svg = document.createElementNS(NS, 'svg');
    svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
    svg.setAttribute('preserveAspectRatio', 'none');
    svg.setAttribute('aria-hidden', 'true');

    const path = document.createElementNS(NS, 'path');
    path.setAttribute('class', 'spark-line');
    path.setAttribute('d', dPath);
    path.setAttribute('stroke', color);
    svg.appendChild(path);

    // 終端のドット
    const dot = document.createElementNS(NS, 'circle');
    dot.setAttribute('class', 'spark-dot');
    dot.setAttribute('cx', pts[pts.length - 1].x);
    dot.setAttribute('cy', pts[pts.length - 1].y);
    dot.setAttribute('r', 3);
    dot.setAttribute('fill', color);
    svg.appendChild(dot);

    card.append(row, valWrap, svg);

    // 描画アニメ
    if (!reduceMotion) {
      const len = path.getTotalLength();
      path.style.strokeDasharray = String(len);
      path.style.strokeDashoffset = String(len);
      dot.style.opacity = '0';
      dot.style.transition = 'opacity .3s ease';
      requestAnimationFrame(() => {
        path.style.transition = 'stroke-dashoffset 1.1s ease';
        path.style.transitionDelay = `${ci * 0.12}s`;
        path.style.strokeDashoffset = '0';
        setTimeout(() => { dot.style.opacity = '1'; }, 1100 + ci * 120);
      });
    }
  });
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。