Canvas棒グラフ

Canvas 2D APIで描く棒グラフ。高DPI対応・出現アニメ・ホバーで値ラベルを表示し、ダッシュボードの数値比較に使えます。

#canvas#chart#animation#interaction

ライブデモ

使用例(お題: カフェ 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。