売上ダッシュボード(KPIカード+期間切り替え+折れ線・棒グラフ)

応用例 中級

この画面パターンについて

KPIカード・期間切り替えタブ・折れ線グラフ・棒グラフを組み合わせた、管理画面で頻出の売上ダッシュボード画面のパターンです。週間/月間のタブを切り替えると、KPIカードの数値も2つのグラフも同じデータソースを参照して一括で更新されます。state.period という1つの値だけで画面全体の表示が決まる仕組みが実装の核心です。

こんな場面で使えます

  • 管理画面のトップページ — 直近の売上・アクセス数などの主要指標を一覧で把握する
  • マーケティング分析画面 — チャネル別・カテゴリ別の比較を週次/月次で切り替えて確認する
  • 経営者向けレポート画面 — KPIの達成状況と推移をひと目で把握する

この画面で使っているUIコンポーネント

#パーツこの画面での役割
1期間切り替えタブ(週間/月間)クリックで state.period を更新し画面全体を再描画
2KPIカード ×3合計売上・平均売上・目標達成率を表示
3前期比サブテキスト(▲▼)合計売上・平均売上カード内に増減を色とアイコンで表現
4折れ線グラフ売上推移(週間=7日分/月間=12ヶ月分)
5棒グラフカテゴリ別売上比較(期間切り替えと連動)
6グラフ凡例折れ線・棒グラフ共通のラベル表示
7ローディング表示JSON fetch完了までの「データを読み込んでいます...」
8データ更新日時表示「最終更新:YYYY-MM-DD HH:mm」の小テキスト

実装のポイント・注意点

画面全体の状態は state = { period: 'week' | 'month' } の1値だけで管理します。タブをクリックすると state.period を書き換えて render() を呼ぶだけで、KPIカード・折れ線グラフ・棒グラフがまとめて更新されます。JSONには weekmonth の2系統のデータをあらかじめ持たせておき、render()data[state.period] を参照するだけで済むようにします。Chart.js のインスタンスは切り替えのたびに破棄・再生成せず、chart.data を更新して chart.update() を呼ぶことで、タブ切り替え時のちらつきを抑えます。

8個のUIコンポーネントをHTML・CSS・バニラJavaScriptのみで組み合わせており、 フレームワーク不要で画面ごとコピペして使えます。

動作サンプル

売上ダッシュボードのデモ画面 動作サンプルを別ウィンドウで確認 ↗

試してみる:

  • 「週間」「月間」タブを切り替えて、KPIカードと2つのグラフが同時に切り替わることを確認
  • 合計売上・平均売上カード内の前期比がプラス(緑▲)とマイナス(赤▼)で色が変わることを確認(週間/月間で見比べてみてください)
  • 棒グラフのカテゴリ別の値も期間によって変わることを確認

そのほかの操作も自由に試してみてください。

サンプルソース

4つのファイルを同じフォルダに保存し、ローカルサーバー(VS Code Live Server等)経由で index.html を開くと動作確認できます。
ファイル名:index.html / style.css / script.js + data/ フォルダに data.json
fetch を使用しているため file:// での直接表示は動作しません(CORSエラー)。 保存時の文字コードは UTF-8 を指定してください。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>売上ダッシュボード</title>
  <link rel="stylesheet" href="./style.css">
  <!-- Chart.js v4(UMD ビルド) -->
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
</head>
<body>

<div class="stats-dashboard-screen">

  <!-- ===== ヘッダー ===== -->
  <header class="dashboard-header">
    <h1 class="screen-title">売上ダッシュボード</h1>
    <p class="last-updated" id="lastUpdated"></p>
    <div class="period-tabs" id="periodTabs" role="tablist" aria-label="期間切り替え">
      <button type="button" class="period-tab is-active" data-period="week" role="tab" aria-selected="true">週間</button>
      <button type="button" class="period-tab" data-period="month" role="tab" aria-selected="false">月間</button>
    </div>
  </header>

  <!-- ===== ローディング ===== -->
  <p class="loading-message" id="loadingMessage">データを読み込んでいます...</p>

  <!-- ===== KPIカード ===== -->
  <section class="kpi-cards" id="kpiCards" hidden aria-label="主要指標">
    <!-- JSで生成:4枚のKPIカード -->
  </section>

  <!-- ===== グラフ ===== -->
  <section class="chart-section" id="chartSection" hidden>
    <div class="chart-block">
      <h2 class="chart-title">売上推移</h2>
      <div class="chart-wrap">
        <canvas id="trendChart" aria-label="売上推移グラフ" role="img"></canvas>
      </div>
    </div>
    <div class="chart-block">
      <h2 class="chart-title">カテゴリ別売上</h2>
      <div class="chart-wrap">
        <canvas id="categoryChart" aria-label="カテゴリ別売上グラフ" role="img"></canvas>
      </div>
    </div>
  </section>

</div>

<script src="./script.js"></script>
</body>
</html>
/* =====================================================
   売上ダッシュボード — スタイル
   スコープ: .stats-dashboard-screen 配下に限定
   ===================================================== */

:root {
  --sd-accent: #2B7FE8;
  --sd-positive: #1FA15C;
  --sd-negative: #E0483D;
  --sd-border: #D8DEE6;
  --sd-bg-card: #ffffff;
  --sd-text-muted: #6B7686;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Hiragino Sans, Meiryo, sans-serif;
  background: #F4F6F9;
  color: #1F2A37;
}

.stats-dashboard-screen {
  max-width: 1080px;
  margin: 0 auto;
  padding: 24px 16px 48px;
}

/* ===== ヘッダー ===== */
.dashboard-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 24px;
}

.screen-title {
  font-size: 22px;
  font-weight: 700;
  margin: 0;
}

.last-updated {
  font-size: 13px;
  color: var(--sd-text-muted);
  margin: 0;
}

.period-tabs {
  display: flex;
  gap: 4px;
  background: #fff;
  border: 1.5px solid var(--sd-border);
  border-radius: 8px;
  padding: 4px;
}

.period-tab {
  padding: 6px 18px;
  font-size: 14px;
  font-weight: 600;
  color: var(--sd-text-muted);
  background: transparent;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}

.period-tab:hover { background: #F0F3F8; }

.period-tab.is-active {
  background: var(--sd-accent);
  color: #fff;
}

/* ===== ローディング ===== */
.loading-message {
  text-align: center;
  color: var(--sd-text-muted);
  font-size: 14px;
  padding: 48px 0;
}

/* ===== KPIカード ===== */
.kpi-cards[hidden] { display: none; }

.kpi-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  margin-bottom: 24px;
}

.kpi-card {
  background: var(--sd-bg-card);
  border: 1.5px solid var(--sd-border);
  border-radius: 10px;
  padding: 16px 18px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.kpi-label {
  font-size: 13px;
  color: var(--sd-text-muted);
}

.kpi-value {
  font-size: 24px;
  font-weight: 700;
}

.kpi-subtext { font-size: 12px; font-weight: 600; margin: 0; }
.kpi-subtext.is-positive { color: var(--sd-positive); }
.kpi-subtext.is-negative { color: var(--sd-negative); }

/* ===== グラフ ===== */
.chart-section[hidden] { display: none; }

.chart-section {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.chart-block {
  background: var(--sd-bg-card);
  border: 1.5px solid var(--sd-border);
  border-radius: 10px;
  padding: 20px;
}

.chart-title {
  font-size: 15px;
  font-weight: 700;
  margin: 0 0 12px;
}

.chart-wrap { height: 320px; }

/* ===== レスポンシブ ===== */
@media (max-width: 768px) {
  .kpi-cards { grid-template-columns: 1fr; }
  .dashboard-header { flex-direction: column; align-items: flex-start; }
}

@media (max-width: 480px) {
  .chart-wrap { height: 260px; }
}
/* =====================================================
   売上ダッシュボードのスクリプト

   仕組み:state.period('week' | 'month')を唯一の情報源にする。
   タブクリックで state.period を書き換えて render() を呼ぶだけ。
   render() が KPIカード・折れ線グラフ・棒グラフをまとめて更新する。

   JSONには week / month 両方のデータをあらかじめ持たせておき、
   render() は data[state.period] を参照するだけにする。
   Chart.js のインスタンスは破棄せず chart.data 更新 + chart.update() で切り替える。
   ===================================================== */

// ===== 設定値 =====
var DEFAULT_PERIOD = 'week';
var CATEGORY_COLORS = ['#2B7FE8', '#1FA15C', '#F2A93B', '#9C6ADE'];

// ===== 状態・データ =====
var state = { period: DEFAULT_PERIOD };
var dashboardData = null;
var trendChart = null;
var categoryChart = null;

var loadingMessage = document.getElementById('loadingMessage');
var kpiCards = document.getElementById('kpiCards');
var chartSection = document.getElementById('chartSection');
var lastUpdated = document.getElementById('lastUpdated');
var periodTabs = document.getElementById('periodTabs');

// ===== 初期化 =====
fetch('./data/data.json')
  .then(function (res) { return res.json(); })
  .then(function (json) {
    dashboardData = json;
    lastUpdated.textContent = '最終更新:' + json.lastUpdated;
    loadingMessage.hidden = true;
    kpiCards.hidden = false;
    chartSection.hidden = false;
    initCharts();
    render();
  })
  .catch(function () {
    loadingMessage.textContent = 'データを読み込めませんでした';
  });

// ===== 期間切り替えタブ =====
// タブクリック → state.period を更新して render() を呼ぶだけ
periodTabs.addEventListener('click', function (e) {
  var btn = e.target.closest('.period-tab');
  if (!btn) return;

  state.period = btn.dataset.period;

  periodTabs.querySelectorAll('.period-tab').forEach(function (tab) {
    var isActive = tab === btn;
    tab.classList.toggle('is-active', isActive);
    tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
  });

  render();
});

// ===== 描画 =====
function render() {
  var current = dashboardData[state.period];
  renderKpiCards(current.kpi);
  updateTrendChart(current.trend);
  updateCategoryChart(current.category);
}

// KPIカード3枚を再生成する。合計売上・平均売上には前期比をサブテキストで添える
function renderKpiCards(kpi) {
  kpiCards.innerHTML = '';

  var isPositive = kpi.changeRate >= 0;
  var changeClass = isPositive ? 'is-positive' : 'is-negative';
  var changeText = '前期比 ' + (isPositive ? '▲ ' : '▼ ') + Math.abs(kpi.changeRate).toFixed(1) + '%';

  var cardDefs = [
    { label: '合計売上', value: '¥' + kpi.total.toLocaleString(), showChange: true },
    { label: '平均売上', value: '¥' + kpi.average.toLocaleString(), showChange: true },
    { label: '目標達成率', value: kpi.goalRate + '%', showChange: false }
  ];

  cardDefs.forEach(function (def) {
    var card = document.createElement('div');
    card.className = 'kpi-card';

    var label = document.createElement('p');
    label.className = 'kpi-label';
    label.textContent = def.label;

    var value = document.createElement('p');
    value.className = 'kpi-value';
    value.textContent = def.value;

    card.appendChild(label);
    card.appendChild(value);

    if (def.showChange) {
      var change = document.createElement('p');
      change.className = 'kpi-subtext ' + changeClass;
      change.textContent = changeText;
      card.appendChild(change);
    }

    kpiCards.appendChild(card);
  });
}

// ===== グラフ初期化(インスタンスは破棄せず再利用する) =====
function initCharts() {
  var initial = dashboardData[DEFAULT_PERIOD];

  trendChart = new Chart(document.getElementById('trendChart'), {
    type: 'line',
    data: {
      labels: initial.trend.labels,
      datasets: [{
        label: '売上',
        data: initial.trend.data,
        borderColor: '#2B7FE8',
        backgroundColor: 'rgba(43, 127, 232, 0.12)',
        tension: 0.3,
        fill: true,
        pointRadius: 4,
        pointHoverRadius: 6
      }]
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      scales: {
        y: { ticks: { callback: function (v) { return (v / 10000).toLocaleString() + '万円'; } } }
      }
    }
  });

  categoryChart = new Chart(document.getElementById('categoryChart'), {
    type: 'bar',
    data: {
      labels: initial.category.labels,
      datasets: [{
        label: 'カテゴリ別売上',
        data: initial.category.data,
        backgroundColor: CATEGORY_COLORS
      }]
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      plugins: { legend: { display: false } },
      scales: {
        y: { ticks: { callback: function (v) { return (v / 10000).toLocaleString() + '万円'; } } }
      }
    }
  });
}

// 期間切り替え時はインスタンスを作り直さず data を差し替えて update() するだけ
function updateTrendChart(trend) {
  trendChart.data.labels = trend.labels;
  trendChart.data.datasets[0].data = trend.data;
  trendChart.update();
}

function updateCategoryChart(category) {
  categoryChart.data.labels = category.labels;
  categoryChart.data.datasets[0].data = category.data;
  categoryChart.update();
}
{
  "lastUpdated": "2026-06-19 09:00",
  "week": {
    "kpi": {
      "total": 1240000,
      "average": 177142,
      "changeRate": 8.4,
      "goalRate": 92
    },
    "trend": {
      "labels": ["6/13", "6/14", "6/15", "6/16", "6/17", "6/18", "6/19"],
      "data": [150000, 168000, 172000, 190000, 210000, 175000, 175000]
    },
    "category": {
      "labels": ["雑貨", "食品", "衣料", "家電"],
      "data": [420000, 360000, 280000, 180000]
    }
  },
  "month": {
    "kpi": {
      "total": 14800000,
      "average": 1233333,
      "changeRate": -3.2,
      "goalRate": 88
    },
    "trend": {
      "labels": ["7月", "8月", "9月", "10月", "11月", "12月", "1月", "2月", "3月", "4月", "5月", "6月"],
      "data": [1100000, 1250000, 1180000, 1320000, 1400000, 1600000, 1350000, 1150000, 1280000, 1300000, 1480000, 1390000]
    },
    "category": {
      "labels": ["雑貨", "食品", "衣料", "家電"],
      "data": [5200000, 4100000, 3300000, 2200000]
    }
  }
}

AI用プロンプト

以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ライブラリ指定やカテゴリ変更など、要件を追記して使うのがおすすめです。

※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

# 売上ダッシュボード 作成依頼

## 概要
KPIカード3枚・期間切り替えタブ(週間/月間)・折れ線グラフ・棒グラフを組み合わせた
売上ダッシュボード画面を作成してください。
JSONをfetchで取得し、タブ切り替えで画面全体のデータが連動して更新されます。

## 要件

### 全体レイアウト
- ヘッダー:タイトル・最終更新日時・期間切り替えタブ(週間/月間)
- KPIカード3枚を横並びで表示(合計売上・平均売上・目標達成率)
- 折れ線グラフ(売上推移)と棒グラフ(カテゴリ別売上)を縦に並べて表示

### 期間切り替えタブ
- 「週間」「月間」の2つのボタン型タブ。選択中をハイライトする
- タブを切り替えると、KPIカード3枚・折れ線グラフ・棒グラフのすべてのデータが
  該当する期間のものに切り替わる

### KPIカード
- 合計売上・平均売上・目標達成率の3枚
- 合計売上・平均売上カードには、数値の下に小さく「前期比」のサブテキストを表示する
- 前期比は正の値なら緑色+▲アイコン、負の値なら赤色+▼アイコンで表示する

### 折れ線グラフ
- 売上推移を表示。週間=直近7日分、月間=直近12ヶ月分のデータに切り替わる

### 棒グラフ
- カテゴリ別売上を表示。カテゴリ軸(4種)は固定だが、値は期間切り替えと連動する

## データ仕様
- JSONファイル(data/data.json)をfetchで取得する
- 構造:{ "lastUpdated": "...", "week": { "kpi": {...}, "trend": {...}, "category": {...} }, "month": { 同形式 } }
- kpi: { "total": 数値, "average": 数値, "changeRate": 数値(正負あり), "goalRate": 数値 }
- trend: { "labels": [...], "data": [...] }(週間=7件、月間=12件)
- category: { "labels": [カテゴリ名4種], "data": [...] }

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:Chart.js v4(https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js)
- state = { period: 'week' | 'month' } の1値で画面全体を管理する
- タブ切り替え時は state を書き換えて render() を呼ぶだけにする
- Chart.js のインスタンスは破棄・再生成せず、chart.data を更新して chart.update() で切り替える
- DOM生成は createElement + textContent を使い、innerHTML に変数を結合しない
- レスポンシブ対応:768px以下でKPIカードを2列、480px以下で1列に切り替える

## 出力形式
HTML・CSS・JavaScript・JSONを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。