採用サイトでよく見る「数字で見る」ページをChart.jsで作る

企業のコーポレートサイトなどを見ていると、社員数や男女比など、会社に関する様々な数値を載せた「数字でみるページ」を見かけることがあります。

画像を使って表現されていることもありますが、ページの表示と同時にグラフが描画されるタイプのモノは、Chart.jsが使われていることが多いです。

今回は、Chart.jsの基本的な使い方から、拡張した使い方までを備忘録として残したいと思います。

目次

Chart.jsについて

Chart.jsはJavaScriptのグラフ描画ライブラリで、もともとはNick Downie氏によって立ち上げられ、現在はコミュニティ主導で継続的にメンテナンスされているようです。

HTMLの<canvas>要素をベースに動作し、折れ線グラフ・棒グラフ・円グラフ・レーダーチャートなど、Webサイトで頻出する主要なグラフ表現をシンプルなAPIで実装できる点が特徴です。

設定はJSONライクなオブジェクトで完結し、レスポンシブ対応、アニメーション、ツールチップ、凡例制御なども標準機能として備えているため、管理画面やマーケティング指標の可視化、採用サイトの「数字で見る」コンテンツなど、実務寄りの用途でも扱いやすいライブラリとして広く利用されているようです。

あわせて読みたい
Chart.js Simple yet flexible JavaScript charting library for the modern web
GitHub
GitHub - chartjs/Chart.js: Simple HTML5 Charts using the tag Simple HTML5 Charts using the tag. Contribute to chartjs/Chart.js development by creating an account on GitHub.

導入手順

1.Chart.js の CDN を HTML に読み込む

まず、Chart.js 本体を CDN 経由で読み込みます。<script> タグを使い、自作の JavaScript よりも前に読み込むことが重要です。

2. グラフ描画用の <canvas> 要素を配置する

HTML 内にグラフを描画するための <canvas> 要素を用意し、id 属性を付与します。Chart.js はこの canvas を対象に描画を行います。

3.必要に応じて CSS で表示サイズやレイアウトを調整する

Chart.js 自体は描画のみを担当するため、グラフの横幅・縦幅やレスポンシブ対応は CSS 側で制御します。canvas やその親要素にスタイルを指定します。

4.JavaScript でグラフ設定とデータを定義する

JavaScript ファイル(または <script> タグ内)で、データ、グラフの種類、軸設定、オプションなどをオブジェクトとして定義します。

5.Chart.js を初期化してグラフを描画する

new Chart() を使い、canvas の取得・データ・オプションを渡してグラフを生成します。これによりブラウザ上にグラフが描画されます。

実装例

今回の実装例では、以下の架空のデータを用意してグラフ化してみました。

  • 年齢構成
  • 職種比率
  • 従業員数推移
  • 平均年収(等級別)
  • 離職率推移
  • 新卒・中途比率

また、通常のChart.jsで表現しきれない部分は、以下のように実装しています。

  1. カスタムプラグインで「バー末尾に人数ラベル」を直接描画
    • afterDatasetsDraw で ctx.fillText() を使い、棒グラフ上に xx名 を表示。
  2. カスタムプラグインで「平均年齢注記」を右上に描画
    • afterDraw でチャート領域右上に「平均年齢 32.1歳」を重ね描き。
  3. ドーナツ内にパーセンテージを自前描画
    • 弧の角度・半径から座標計算し、中央寄りに % ラベルを描画。roleLabel では小さすぎるスライス(6%未満)を非表示にする工夫あり。

動作サンプル

See the Pen Chart.js DEMO by Yoshihiro Hotta (@yoshihiro-hotta) on CodePen.

HTML

<section class="metrics">
  <article class="chart-card">
    <h2 class="chart-card__title">年齢構成</h2>
    <div class="chart-card__canvas">
      <canvas id="ageChart"></canvas>
    </div>
  </article>

  <article class="chart-card">
    <h2 class="chart-card__title">職種比率</h2>
    <div class="chart-card__canvas">
      <canvas id="roleChart"></canvas>
    </div>
  </article>

  <article class="chart-card">
    <h2 class="chart-card__title">従業員数推移</h2>
    <div class="chart-card__canvas">
      <canvas id="headcountChart"></canvas>
    </div>
  </article>

  <article class="chart-card">
    <h2 class="chart-card__title">平均年収(等級別)</h2>
    <div class="chart-card__canvas">
      <canvas id="salaryChart"></canvas>
    </div>
  </article>

  <article class="chart-card">
    <h2 class="chart-card__title">離職率推移</h2>
    <div class="chart-card__canvas">
      <canvas id="turnoverChart"></canvas>
    </div>
  </article>

  <article class="chart-card">
    <h2 class="chart-card__title">新卒・中途比率</h2>
    <div class="chart-card__canvas">
      <canvas id="hiredRatioChart"></canvas>
    </div>
  </article>
</section>

CSS

:root {
  --bg: #f0f9ff;
  --panel: #ffffff;
  --border: rgba(59, 130, 246, 0.18);
  --text: #1e293b;
  --muted: #64748b;
  --accent: #3b82f6;
  --radius: 14px;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: system-ui, -apple-system, "Noto Sans JP", sans-serif;
}

.page {
  max-width: 1200px;
  margin: 0 auto;
  padding: 24px;
}

.page__header {
  margin-bottom: 24px;
}

.page__title {
  margin: 0 0 4px;
}

.page__subtitle {
  margin: 0;
  color: var(--muted);
  font-size: 14px;
}

.metrics {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px;
  padding: 20px;
}

.chart-card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px;
  box-shadow: 0 4px 20px rgba(59, 130, 246, 0.08);
}

.chart-card__title {
  font-size: 15px;
  font-weight: 700;
  margin: 0 0 12px;
  color: var(--accent);
  letter-spacing: 0.02em;
}

.chart-card__canvas {
  height: 300px;
}

canvas {
  width: 100% !important;
  height: 100% !important;
}

@media (max-width: 700px) {
  .metrics {
    grid-template-columns: 1fr;
  }
}

JavaScript

/**
 * @typedef {import('chart.js').Chart} Chart
 * @typedef {import('chart.js').ChartConfiguration} ChartConfiguration
 */

/**
 * Canvas取得ユーティリティ
 * @param {string} id
 * @returns {HTMLCanvasElement}
 */
const getCanvas = (id) => {
  const el = document.getElementById(id);
  if (!(el instanceof HTMLCanvasElement)) {
    throw new Error(`Canvas not found: ${id}`);
  }
  return el;
};

/** カラーパレット */
const C = {
  blue:   "#3B82F6",
  sky:    "#38BDF8",
  teal:   "#14B8A6",
  green:  "#22C55E",
  lime:   "#84CC16",
  violet: "#8B5CF6",
  pink:   "#EC4899",
  orange: "#F97316",
};

const GRID   = "rgba(0,0,0,0.07)";
const TICK   = "#64748b";
const LEGEND = "#1e293b";

/** 共通オプション */
const baseOptions = {
  responsive: true,
  maintainAspectRatio: false,
  plugins: {
    legend: {
      position: "bottom",
      labels: { color: LEGEND, font: { size: 12 } },
    },
    tooltip: {
      enabled: true,
    },
  },
};

const lightScale = (extra = {}) => ({
  grid: { color: GRID },
  ticks: { color: TICK },
  ...extra,
});

/* -----------------------
 * 1) 年齢構成(横棒)
 * --------------------- */
const ageLabelPlugin = {
  id: "ageLabel",
  afterDatasetsDraw(chart) {
    const { ctx } = chart;
    ctx.save();
    ctx.font = "bold 13px system-ui, -apple-system, sans-serif";
    ctx.fillStyle = "#1e293b";
    ctx.textAlign = "left";
    ctx.textBaseline = "middle";

    chart.data.datasets.forEach((dataset, datasetIndex) => {
      chart.getDatasetMeta(datasetIndex).data.forEach((bar, index) => {
        ctx.fillText(`${dataset.data[index]}名`, bar.x + 6, bar.y);
      });
    });

    ctx.restore();
  },
};

const ageAvgPlugin = {
  id: "ageAvg",
  afterDraw(chart) {
    const { ctx, chartArea: { right, top } } = chart;
    ctx.save();
    ctx.font = "bold 13px system-ui, -apple-system, sans-serif";
    ctx.fillStyle = "#3B82F6";
    ctx.textAlign = "right";
    ctx.fillText("平均年齢 32.1歳", right, top + 16);
    ctx.restore();
  },
};

new Chart(getCanvas("ageChart"), {
  type: "bar",
  data: {
    labels: ["20代", "30代", "40代以上"],
    datasets: [
      {
        label: "人数",
        data: [60, 89, 21],
        backgroundColor: ["#38BDF8", "#3B82F6", "#1D4ED8"],
        borderRadius: 6,
      },
    ],
  },
  options: {
    ...baseOptions,
    indexAxis: "y",
    scales: {
      x: {
        ...lightScale(),
        beginAtZero: true,
        ticks: {
          color: TICK,
          callback: (v) => `${v}名`,
        },
      },
      y: lightScale(),
    },
  },
  plugins: [ageLabelPlugin, ageAvgPlugin],
});

/* -----------------------
 * 2) 職種比率(ドーナツ)
 * --------------------- */
const roleLabelPlugin = {
  id: "roleLabel",
  afterDatasetsDraw(chart) {
    const { ctx } = chart;
    const dataset = chart.data.datasets[0];
    const total = dataset.data.reduce((a, b) => a + b, 0);

    ctx.save();
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";

    chart.getDatasetMeta(0).data.forEach((arc, index) => {
      const pct = Math.round((dataset.data[index] / total) * 100);
      if (pct < 6) return; // スライスが小さすぎる場合はスキップ

      const midAngle = (arc.startAngle + arc.endAngle) / 2;
      const midRadius = (arc.outerRadius + arc.innerRadius) / 2;
      const x = arc.x + Math.cos(midAngle) * midRadius;
      const y = arc.y + Math.sin(midAngle) * midRadius;

      ctx.font = "bold 13px system-ui, -apple-system, sans-serif";
      ctx.fillStyle = "#ffffff";
      ctx.fillText(`${pct}%`, x, y);
    });

    ctx.restore();
  },
};

new Chart(getCanvas("roleChart"), {
  type: "doughnut",
  data: {
    labels: ["エンジニア", "営業", "マーケ", "CS", "その他"],
    datasets: [
      {
        data: [62, 28, 18, 14, 8],
        backgroundColor: [C.blue, C.teal, C.green, C.violet, C.sky],
        borderWidth: 2,
        borderColor: "#ffffff",
      },
    ],
  },
  options: {
    ...baseOptions,
    cutout: "60%",
  },
  plugins: [roleLabelPlugin],
});

/* -----------------------
 * 3) 従業員数推移(面グラフ)
 * --------------------- */
new Chart(getCanvas("headcountChart"), {
  type: "line",
  data: {
    labels: ["2021", "2022", "2023", "2024", "2025"],
    datasets: [
      {
        label: "従業員数",
        data: [120, 148, 175, 210, 248],
        tension: 0.3,
        fill: true,
        borderColor: C.blue,
        backgroundColor: "rgba(59,130,246,0.12)",
        pointBackgroundColor: C.blue,
        pointRadius: 5,
      },
    ],
  },
  options: {
    ...baseOptions,
    scales: {
      y: {
        ...lightScale(),
        beginAtZero: false,
        ticks: { color: TICK, callback: (v) => `${v}名` },
      },
      x: lightScale(),
    },
  },
});

/* -----------------------
 * 4) 平均年収(等級別・横棒)
 * --------------------- */
new Chart(getCanvas("salaryChart"), {
  type: "bar",
  data: {
    labels: ["ジュニア", "ミドル", "シニア", "リード", "マネージャー"],
    datasets: [
      {
        label: "平均年収",
        data: [380, 480, 620, 780, 950],
        backgroundColor: ["#BFDBFE", "#93C5FD", "#60A5FA", "#3B82F6", "#1D4ED8"],
        borderRadius: 6,
      },
    ],
  },
  options: {
    ...baseOptions,
    indexAxis: "y",
    scales: {
      x: {
        ...lightScale(),
        beginAtZero: true,
        ticks: { color: TICK, callback: (v) => `${v}万` },
      },
      y: lightScale(),
    },
  },
});

/* -----------------------
 * 5) 離職率推移(折れ線)
 * --------------------- */
new Chart(getCanvas("turnoverChart"), {
  type: "line",
  data: {
    labels: ["2021", "2022", "2023", "2024", "2025"],
    datasets: [
      {
        label: "離職率",
        data: [5.2, 4.8, 4.1, 3.6, 2.9],
        tension: 0.3,
        fill: true,
        borderColor: C.green,
        backgroundColor: "rgba(34,197,94,0.12)",
        pointBackgroundColor: C.green,
        pointRadius: 5,
      },
    ],
  },
  options: {
    ...baseOptions,
    scales: {
      y: {
        ...lightScale(),
        beginAtZero: true,
        max: 8,
        ticks: { color: TICK, callback: (v) => `${v}%` },
      },
      x: lightScale(),
    },
  },
});

/* -----------------------
 * 6) 新卒・中途比率(ドーナツ)
 * --------------------- */
const hiredRatioLabelPlugin = {
  id: "hiredRatioLabel",
  afterDatasetsDraw(chart) {
    const { ctx } = chart;
    const dataset = chart.data.datasets[0];
    const total = dataset.data.reduce((a, b) => a + b, 0);

    ctx.save();
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";

    chart.getDatasetMeta(0).data.forEach((arc, index) => {
      const pct = Math.round((dataset.data[index] / total) * 100);
      const midAngle = (arc.startAngle + arc.endAngle) / 2;
      const midRadius = (arc.outerRadius + arc.innerRadius) / 2;
      const x = arc.x + Math.cos(midAngle) * midRadius;
      const y = arc.y + Math.sin(midAngle) * midRadius;

      ctx.font = "bold 13px system-ui, -apple-system, sans-serif";
      ctx.fillStyle = "#ffffff";
      ctx.fillText(`${pct}%`, x, y);
    });

    ctx.restore();
  },
};

new Chart(getCanvas("hiredRatioChart"), {
  type: "doughnut",
  data: {
    labels: ["新卒入社", "中途入社"],
    datasets: [
      {
        data: [42, 58],
        backgroundColor: [C.blue, C.teal],
        borderWidth: 2,
        borderColor: "#ffffff",
      },
    ],
  },
  options: {
    ...baseOptions,
    cutout: "60%",
  },
  plugins: [hiredRatioLabelPlugin],
});

最後に

今回の実装を通してChart.jsの基本的な使い方を理解できました。

採用ページで見るようなリッチな見た目に近づけたい場合は、Chart.jsそのものの機能だけではなく、ある程度拡張する必要があることもわかりました。

WordPressサイトで利用する場合は、カスタムフィールドに入力した値がグラフに反映されるような仕組みにすると、管理が楽かもしれませんね。

管理画面でどのグラフを使うか、なにを表現したグラフか、何色でみせるか…などを指定できるように実装すると、採用チームがコードに触れることなく運用できるので、このあたりも考慮できると良いかもしれません。

次回は、WordPress用に実装したものを記事にしたいと思います!

目次