スパークライン
data属性の値からSVGの極小グラフを生成。前回比バッジ付きで、KPIカードやダッシュボードの省スペース指標に向きます。
ライブデモ
使用例(お題: 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。