Canvas棒グラフ
Canvas 2D APIで描く棒グラフ。高DPI対応・出現アニメ・ホバーで値ラベルを表示し、ダッシュボードの数値比較に使えます。
ライブデモ
使用例(お題: カフェ MOON BREW)
この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- MOON BREW:店内ダッシュボード。人気メニューの売上をCanvas棒グラフで比較 -->
<section class="mb-dash">
<header class="mb-bar">
<div class="mb-brand"><span class="mb-cup">☕</span> MOON BREW</div>
<span class="mb-store">青山店 ・ 今週の売上</span>
</header>
<div class="mb-panel">
<div class="mb-panel__head">
<h2 class="mb-panel__title">人気メニュー TOP7</h2>
<p class="mb-panel__sub">バーにカーソルを合わせると杯数を表示</p>
</div>
<!-- Canvas棒グラフ本体(高DPI対応・JSで描画) -->
<canvas id="mbBarCanvas" class="mb-canvas" role="img" aria-label="メニュー別売上杯数の棒グラフ"></canvas>
</div>
<footer class="mb-foot">
<span class="mb-foot__num">2,418 杯</span>
<span class="mb-foot__lab">今週の総提供数</span>
</footer>
</section>
CSS
/* MOON BREW:人気メニュー売上ダッシュボード(Canvas棒グラフが主役) */
:root {
--cream: #f5ede1;
--brown: #2b1d12;
--amber: #c98a3b;
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 400px;
font-family: "Hiragino Kaku Gothic ProN", "Yu Gothic", system-ui, sans-serif;
background: var(--cream);
color: var(--brown);
}
.mb-dash { height: 400px; padding: 16px 20px; display: flex; flex-direction: column; }
.mb-bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.mb-brand {
font-family: "Hiragino Mincho ProN", serif;
font-weight: 700; font-size: 16px; letter-spacing: 0.1em;
display: flex; align-items: center; gap: 7px;
}
.mb-cup { font-size: 17px; }
.mb-store { font-size: 11px; color: #8a755e; letter-spacing: 0.06em; }
/* グラフを載せるカード */
.mb-panel {
flex: 1;
background: #fffaf2;
border: 1px solid #e7dccb;
border-radius: 14px;
padding: 14px 16px 8px;
box-shadow: 0 8px 20px rgba(43, 29, 18, 0.06);
display: flex; flex-direction: column;
}
.mb-panel__head { margin-bottom: 4px; }
.mb-panel__title { margin: 0; font-size: 15px; font-weight: 700; }
.mb-panel__sub { margin: 2px 0 0; font-size: 10px; color: #a08a6f; }
.mb-canvas { flex: 1; width: 100%; min-height: 0; display: block; }
.mb-foot {
display: flex; align-items: baseline; gap: 8px;
margin-top: 12px; padding: 0 4px;
}
.mb-foot__num { font-size: 20px; font-weight: 800; color: var(--amber); }
.mb-foot__lab { font-size: 11px; color: #8a755e; }
JavaScript
// MOON BREW:メニュー別売上をCanvas棒グラフで描画(高DPI・出現アニメ・ホバー)
(() => {
const canvas = document.getElementById('mbBarCanvas');
if (!canvas) return; // null安全
const ctx = canvas.getContext('2d');
if (!ctx) return;
// 人気メニューと提供杯数(ダミー)
const data = [
{ label: 'ラテ', value: 612 },
{ label: 'ドリップ', value: 498 },
{ label: 'カプチ', value: 374 },
{ label: 'モカ', value: 281 },
{ label: '抹茶', value: 233 },
{ label: 'アメリ', value: 268 },
{ label: 'ココア', value: 152 },
];
const maxVal = Math.max(...data.map((d) => d.value));
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let progress = reduceMotion ? 1 : 0; // 0→1で棒が伸びる
let hoverIndex = -1;
let dpr = 1, W = 0, H = 0;
// CSSサイズに合わせ内部解像度を設定(ぼやけ防止)
function resize() {
dpr = Math.max(1, window.devicePixelRatio || 1);
const rect = canvas.getBoundingClientRect();
W = rect.width; H = rect.height;
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
const pad = { top: 16, right: 12, bottom: 26, left: 38 };
function draw() {
ctx.clearRect(0, 0, W, H);
const plotW = W - pad.left - pad.right;
const plotH = H - pad.top - pad.bottom;
// 水平グリッドと目盛り
ctx.strokeStyle = 'rgba(43,29,18,0.08)';
ctx.fillStyle = 'rgba(43,29,18,0.45)';
ctx.lineWidth = 1;
ctx.font = '10px system-ui, sans-serif';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
const steps = 4;
for (let i = 0; i <= steps; i++) {
const y = pad.top + (plotH / steps) * i;
const val = Math.round((maxVal / steps) * (steps - i));
ctx.beginPath();
ctx.moveTo(pad.left, y);
ctx.lineTo(W - pad.right, y);
ctx.stroke();
ctx.fillText(String(val), pad.left - 6, y);
}
// 棒
const n = data.length;
const slot = plotW / n;
const barW = slot * 0.56;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
data.forEach((d, i) => {
const ratio = (d.value / maxVal) * progress;
const barH = plotH * ratio;
const x = pad.left + slot * i + (slot - barW) / 2;
const y = pad.top + plotH - barH;
// 琥珀色グラデ。ホバー時は濃く強調
const grad = ctx.createLinearGradient(0, y, 0, pad.top + plotH);
if (i === hoverIndex) {
grad.addColorStop(0, '#e0a85a');
grad.addColorStop(1, '#c98a3b');
} else {
grad.addColorStop(0, '#d59b50');
grad.addColorStop(1, '#a86f2c');
}
ctx.fillStyle = grad;
roundRect(ctx, x, y, barW, barH, 5);
ctx.fill();
// ホバー中は杯数ラベルを上に
if (i === hoverIndex && barH > 4) {
ctx.fillStyle = '#2b1d12';
ctx.font = 'bold 11px system-ui, sans-serif';
ctx.textBaseline = 'bottom';
ctx.fillText(`${d.value}杯`, x + barW / 2, y - 4);
ctx.textBaseline = 'top';
}
// x軸ラベル
ctx.fillStyle = 'rgba(43,29,18,0.7)';
ctx.font = '10px system-ui, sans-serif';
ctx.fillText(d.label, x + barW / 2, pad.top + plotH + 7);
});
}
// 角丸矩形ヘルパー
function roundRect(c, x, y, w, h, r) {
const rr = Math.min(r, w / 2, Math.max(0, h / 2));
c.beginPath();
c.moveTo(x + rr, y);
c.arcTo(x + w, y, x + w, y + h, rr);
c.arcTo(x + w, y + h, x, y + h, 0);
c.arcTo(x, y + h, x, y, 0);
c.arcTo(x, y, x + w, y, rr);
c.closePath();
}
// 出現アニメ
function animate() {
if (progress < 1) {
progress = Math.min(1, progress + 0.03);
draw();
requestAnimationFrame(animate);
} else {
draw();
}
}
// マウス位置から対象の棒を特定
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const plotW = W - pad.left - pad.right;
const slot = plotW / data.length;
const idx = Math.floor((mx - pad.left) / slot);
const next = (mx >= pad.left && idx >= 0 && idx < data.length) ? idx : -1;
if (next !== hoverIndex) { hoverIndex = next; draw(); }
});
canvas.addEventListener('mouseleave', () => {
if (hoverIndex !== -1) { hoverIndex = -1; draw(); }
});
window.addEventListener('resize', () => { resize(); draw(); });
resize();
animate();
})();
コード
HTML
<div class="dv-wrap">
<figure class="dv-card">
<figcaption class="dv-head">
<h2 class="dv-title">月間アクティブユーザー</h2>
<p class="dv-sub">Canvas 2D で描く棒グラフ(マウスでハイライト)</p>
</figcaption>
<!-- グラフ本体。Canvasは高DPI対応でJSから描画する -->
<canvas id="barCanvas" class="dv-canvas" role="img" aria-label="月別ユーザー数の棒グラフ"></canvas>
</figure>
</div>
CSS
/* カラーとサイズの一元管理 */
:root {
--dv-bg-a: #0f172a;
--dv-bg-b: #1e293b;
--dv-ink: #e2e8f0;
--dv-sub: #94a3b8;
--dv-accent: #38bdf8;
--dv-radius: 18px;
}
* { 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(1200px 600px at 20% -10%, #243049 0%, transparent 60%),
linear-gradient(135deg, var(--dv-bg-a), var(--dv-bg-b));
}
.dv-wrap {
width: min(92vw, 720px);
padding: 20px;
}
.dv-card {
margin: 0;
padding: 22px 24px 16px;
border-radius: var(--dv-radius);
background: rgba(15, 23, 42, 0.55);
border: 1px solid rgba(148, 163, 184, 0.18);
box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.7);
backdrop-filter: blur(6px);
}
.dv-head { margin-bottom: 12px; }
.dv-title {
margin: 0;
font-size: clamp(18px, 3.4vw, 22px);
letter-spacing: 0.02em;
}
.dv-sub {
margin: 4px 0 0;
font-size: 13px;
color: var(--dv-sub);
}
.dv-canvas {
display: block;
width: 100%;
height: 196px;
border-radius: 12px;
}
JavaScript
// Canvasで棒グラフを描画。高DPI対応・出現アニメ・ホバーハイライト付き
(() => {
const canvas = document.getElementById('barCanvas');
if (!canvas) return; // null安全
const ctx = canvas.getContext('2d');
if (!ctx) return;
// ダミーデータ(ラベルと値)
const data = [
{ label: '1月', value: 42 },
{ label: '2月', value: 58 },
{ label: '3月', value: 73 },
{ label: '4月', value: 49 },
{ label: '5月', value: 88 },
{ label: '6月', value: 95 },
{ label: '7月', value: 67 },
];
const maxVal = Math.max(...data.map((d) => d.value));
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let progress = reduceMotion ? 1 : 0; // 0→1で伸びる
let hoverIndex = -1;
let dpr = 1, W = 0, H = 0;
// CSSサイズに合わせて内部解像度を設定(ぼやけ防止)
function resize() {
dpr = Math.max(1, window.devicePixelRatio || 1);
const rect = canvas.getBoundingClientRect();
W = rect.width;
H = rect.height;
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
const pad = { top: 18, right: 16, bottom: 30, left: 36 };
function draw() {
ctx.clearRect(0, 0, W, H);
const plotW = W - pad.left - pad.right;
const plotH = H - pad.top - pad.bottom;
// 水平グリッド線と目盛り
ctx.strokeStyle = 'rgba(148,163,184,0.18)';
ctx.fillStyle = 'rgba(148,163,184,0.75)';
ctx.lineWidth = 1;
ctx.font = '11px system-ui, sans-serif';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
const steps = 4;
for (let i = 0; i <= steps; i++) {
const y = pad.top + (plotH / steps) * i;
const val = Math.round((maxVal / steps) * (steps - i));
ctx.beginPath();
ctx.moveTo(pad.left, y);
ctx.lineTo(W - pad.right, y);
ctx.stroke();
ctx.fillText(String(val), pad.left - 8, y);
}
// 棒の描画
const n = data.length;
const slot = plotW / n;
const barW = slot * 0.56;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
data.forEach((d, i) => {
const ratio = (d.value / maxVal) * progress;
const barH = plotH * ratio;
const x = pad.left + slot * i + (slot - barW) / 2;
const y = pad.top + plotH - barH;
// ホバー時は明るいグラデ、通常は控えめなグラデ
const grad = ctx.createLinearGradient(0, y, 0, pad.top + plotH);
if (i === hoverIndex) {
grad.addColorStop(0, '#7dd3fc');
grad.addColorStop(1, '#38bdf8');
} else {
grad.addColorStop(0, '#38bdf8');
grad.addColorStop(1, '#2563eb');
}
ctx.fillStyle = grad;
roundRect(ctx, x, y, barW, barH, 6);
ctx.fill();
// ホバー中の棒は値ラベルを上に表示
if (i === hoverIndex && barH > 4) {
ctx.fillStyle = '#e2e8f0';
ctx.font = 'bold 12px system-ui, sans-serif';
ctx.textBaseline = 'bottom';
ctx.fillText(String(d.value), x + barW / 2, y - 4);
ctx.textBaseline = 'top';
}
// x軸ラベル
ctx.fillStyle = 'rgba(226,232,240,0.8)';
ctx.font = '11px system-ui, sans-serif';
ctx.fillText(d.label, x + barW / 2, pad.top + plotH + 8);
});
}
// 角丸矩形のヘルパー
function roundRect(c, x, y, w, h, r) {
const rr = Math.min(r, w / 2, Math.max(0, h / 2));
c.beginPath();
c.moveTo(x + rr, y);
c.arcTo(x + w, y, x + w, y + h, rr);
c.arcTo(x + w, y + h, x, y + h, 0);
c.arcTo(x, y + h, x, y, 0);
c.arcTo(x, y, x + w, y, rr);
c.closePath();
}
// 出現アニメーション
function animate() {
if (progress < 1) {
progress = Math.min(1, progress + 0.03);
draw();
requestAnimationFrame(animate);
} else {
draw();
}
}
// マウス位置から対象の棒を特定
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const plotW = W - pad.left - pad.right;
const slot = plotW / data.length;
const idx = Math.floor((mx - pad.left) / slot);
const next = (mx >= pad.left && idx >= 0 && idx < data.length) ? idx : -1;
if (next !== hoverIndex) {
hoverIndex = next;
draw();
}
});
canvas.addEventListener('mouseleave', () => {
if (hoverIndex !== -1) { hoverIndex = -1; draw(); }
});
window.addEventListener('resize', () => { resize(); draw(); });
resize();
animate();
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「Canvas棒グラフ」の効果を追加してください。
# 追加してほしい効果
Canvas棒グラフ(データ可視化)
Canvas 2D APIで描く棒グラフ。高DPI対応・出現アニメ・ホバーで値ラベルを表示し、ダッシュボードの数値比較に使えます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="dv-wrap">
<figure class="dv-card">
<figcaption class="dv-head">
<h2 class="dv-title">月間アクティブユーザー</h2>
<p class="dv-sub">Canvas 2D で描く棒グラフ(マウスでハイライト)</p>
</figcaption>
<!-- グラフ本体。Canvasは高DPI対応でJSから描画する -->
<canvas id="barCanvas" class="dv-canvas" role="img" aria-label="月別ユーザー数の棒グラフ"></canvas>
</figure>
</div>
【CSS】
/* カラーとサイズの一元管理 */
:root {
--dv-bg-a: #0f172a;
--dv-bg-b: #1e293b;
--dv-ink: #e2e8f0;
--dv-sub: #94a3b8;
--dv-accent: #38bdf8;
--dv-radius: 18px;
}
* { 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(1200px 600px at 20% -10%, #243049 0%, transparent 60%),
linear-gradient(135deg, var(--dv-bg-a), var(--dv-bg-b));
}
.dv-wrap {
width: min(92vw, 720px);
padding: 20px;
}
.dv-card {
margin: 0;
padding: 22px 24px 16px;
border-radius: var(--dv-radius);
background: rgba(15, 23, 42, 0.55);
border: 1px solid rgba(148, 163, 184, 0.18);
box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.7);
backdrop-filter: blur(6px);
}
.dv-head { margin-bottom: 12px; }
.dv-title {
margin: 0;
font-size: clamp(18px, 3.4vw, 22px);
letter-spacing: 0.02em;
}
.dv-sub {
margin: 4px 0 0;
font-size: 13px;
color: var(--dv-sub);
}
.dv-canvas {
display: block;
width: 100%;
height: 196px;
border-radius: 12px;
}
【JavaScript】
// Canvasで棒グラフを描画。高DPI対応・出現アニメ・ホバーハイライト付き
(() => {
const canvas = document.getElementById('barCanvas');
if (!canvas) return; // null安全
const ctx = canvas.getContext('2d');
if (!ctx) return;
// ダミーデータ(ラベルと値)
const data = [
{ label: '1月', value: 42 },
{ label: '2月', value: 58 },
{ label: '3月', value: 73 },
{ label: '4月', value: 49 },
{ label: '5月', value: 88 },
{ label: '6月', value: 95 },
{ label: '7月', value: 67 },
];
const maxVal = Math.max(...data.map((d) => d.value));
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let progress = reduceMotion ? 1 : 0; // 0→1で伸びる
let hoverIndex = -1;
let dpr = 1, W = 0, H = 0;
// CSSサイズに合わせて内部解像度を設定(ぼやけ防止)
function resize() {
dpr = Math.max(1, window.devicePixelRatio || 1);
const rect = canvas.getBoundingClientRect();
W = rect.width;
H = rect.height;
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
const pad = { top: 18, right: 16, bottom: 30, left: 36 };
function draw() {
ctx.clearRect(0, 0, W, H);
const plotW = W - pad.left - pad.right;
const plotH = H - pad.top - pad.bottom;
// 水平グリッド線と目盛り
ctx.strokeStyle = 'rgba(148,163,184,0.18)';
ctx.fillStyle = 'rgba(148,163,184,0.75)';
ctx.lineWidth = 1;
ctx.font = '11px system-ui, sans-serif';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
const steps = 4;
for (let i = 0; i <= steps; i++) {
const y = pad.top + (plotH / steps) * i;
const val = Math.round((maxVal / steps) * (steps - i));
ctx.beginPath();
ctx.moveTo(pad.left, y);
ctx.lineTo(W - pad.right, y);
ctx.stroke();
ctx.fillText(String(val), pad.left - 8, y);
}
// 棒の描画
const n = data.length;
const slot = plotW / n;
const barW = slot * 0.56;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
data.forEach((d, i) => {
const ratio = (d.value / maxVal) * progress;
const barH = plotH * ratio;
const x = pad.left + slot * i + (slot - barW) / 2;
const y = pad.top + plotH - barH;
// ホバー時は明るいグラデ、通常は控えめなグラデ
const grad = ctx.createLinearGradient(0, y, 0, pad.top + plotH);
if (i === hoverIndex) {
grad.addColorStop(0, '#7dd3fc');
grad.addColorStop(1, '#38bdf8');
} else {
grad.addColorStop(0, '#38bdf8');
grad.addColorStop(1, '#2563eb');
}
ctx.fillStyle = grad;
roundRect(ctx, x, y, barW, barH, 6);
ctx.fill();
// ホバー中の棒は値ラベルを上に表示
if (i === hoverIndex && barH > 4) {
ctx.fillStyle = '#e2e8f0';
ctx.font = 'bold 12px system-ui, sans-serif';
ctx.textBaseline = 'bottom';
ctx.fillText(String(d.value), x + barW / 2, y - 4);
ctx.textBaseline = 'top';
}
// x軸ラベル
ctx.fillStyle = 'rgba(226,232,240,0.8)';
ctx.font = '11px system-ui, sans-serif';
ctx.fillText(d.label, x + barW / 2, pad.top + plotH + 8);
});
}
// 角丸矩形のヘルパー
function roundRect(c, x, y, w, h, r) {
const rr = Math.min(r, w / 2, Math.max(0, h / 2));
c.beginPath();
c.moveTo(x + rr, y);
c.arcTo(x + w, y, x + w, y + h, rr);
c.arcTo(x + w, y + h, x, y + h, 0);
c.arcTo(x, y + h, x, y, 0);
c.arcTo(x, y, x + w, y, rr);
c.closePath();
}
// 出現アニメーション
function animate() {
if (progress < 1) {
progress = Math.min(1, progress + 0.03);
draw();
requestAnimationFrame(animate);
} else {
draw();
}
}
// マウス位置から対象の棒を特定
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const plotW = W - pad.left - pad.right;
const slot = plotW / data.length;
const idx = Math.floor((mx - pad.left) / slot);
const next = (mx >= pad.left && idx >= 0 && idx < data.length) ? idx : -1;
if (next !== hoverIndex) {
hoverIndex = next;
draw();
}
});
canvas.addEventListener('mouseleave', () => {
if (hoverIndex !== -1) { hoverIndex = -1; draw(); }
});
window.addEventListener('resize', () => { resize(); draw(); });
resize();
animate();
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。