開発情報・ナレッジ

投稿者: ShiningStar株式会社 2025年12月8日 (月)

非同期通信を使ったグラフ付きダッシュボードを作成するサンプルプログラム

カスタムAPIを使ってリアルタイム売上ダッシュボードを作成するサンプルプログラムを紹介します。
本記事は「最小構成で動作検証 → 実DB連携」の2段階で解説します。
開発初期はカスタムAPI(モックデータ)でUIと動作をすばやく検証し、
その後に本番データへ差し替えることで開発スピードと品質の両立を図れます。


簡易実装パターン(モックデータ)

まずは実DBを使わずにモックデータを返すAPIで画面イメージと基本動作を確認します。
HTML描画用の閲覧フォームとPHP設置用のフォームの二枚のフォームを予め作成してください。
UI検討や初期実装では、この方法で素早く動作を確認することを推奨します。

HTML

以下のコードを閲覧用のフォームに配置してください。

<!DOCTYPE html>
<html id="SMP_STYLE">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<title>リアルタイムデータ可視化ダッシュボード</title>

<style type="text/css">
body {
      font-family: 'Helvetica Neue', Arial, sans-serif;
      margin: 0;
      padding: 20px;
      background-color: #f5f7fa;
      color: #333;
    }
    
    .dashboard-container {
      max-width: 1200px;
      margin: 0 auto;
    }
    
    .dashboard-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 20px;
    }
    
    .dashboard-title {
      margin: 0;
      color: #2c3e50;
    }
    
    .dashboard-controls {
      display: flex;
      gap: 10px;
    }
    
    .dashboard-refresh {
      background-color: #3498db;
      color: white;
      border: none;
      padding: 8px 15px;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
      transition: background-color 0.3s;
    }
    
    .dashboard-refresh:hover {
      background-color: #2980b9;
    }
    
    .dashboard-interval {
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 14px;
    }
    
    .dashboard-status {
      font-size: 14px;
      color: #7f8c8d;
    }
    
    .dashboard-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
      gap: 20px;
      margin-bottom: 20px;
    }
    
    .dashboard-card {
      background-color: white;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
      padding: 20px;
    }
    
    .dashboard-card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 15px;
    }
    
    .dashboard-card-title {
      margin: 0;
      font-size: 18px;
      color: #2c3e50;
    }
    
    .dashboard-card-value {
      font-size: 24px;
      font-weight: bold;
      color: #3498db;
    }
    
    .dashboard-chart-container {
      position: relative;
      height: 300px;
    }
    
    .dashboard-table {
      width: 100%;
      border-collapse: collapse;
    }
    
    .dashboard-table th,
    .dashboard-table td {
      padding: 12px 15px;
      text-align: left;
      border-bottom: 1px solid #ddd;
    }
    
    .dashboard-table th {
      background-color: #f8f9fa;
      font-weight: bold;
      color: #2c3e50;
    }
    
    .dashboard-table tr:hover {
      background-color: #f5f7fa;
    }
    
    .dashboard-loading {
      display: none;
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(255, 255, 255, 0.7);
      z-index: 1000;
      justify-content: center;
      align-items: center;
    }
    
    .dashboard-spinner {
      border: 4px solid rgba(0, 0, 0, 0.1);
      border-radius: 50%;
      border-top: 4px solid #3498db;
      width: 40px;
      height: 40px;
      animation: spin 1s linear infinite;
    }
    
    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }
    
    .positive-trend {
      color: #27ae60;
    }
    
    .negative-trend {
      color: #e74c3c;
    }
    
    .trend-indicator {
      margin-left: 5px;
    }
    
    .dashboard-summary {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 20px;
      margin-bottom: 20px;
    }
    
    .summary-card {
      background-color: white;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
      padding: 20px;
      text-align: center;
    }
    
    .summary-title {
      font-size: 14px;
      color: #7f8c8d;
      margin-bottom: 10px;
    }
    
    .summary-value {
      font-size: 24px;
      font-weight: bold;
      color: #2c3e50;
      margin-bottom: 5px;
    }
    
    .summary-change {
      font-size: 14px;
    }
</style>

</head>
<body class="body">
  <div class="dashboard-container">
    <div class="dashboard-header">
      <h1 class="dashboard-title">リアルタイムデータ可視化ダッシュボード</h1>
      <div class="dashboard-controls">
        <select id="interval-select" class="dashboard-interval">
          <option value="5000">5秒</option>
          <option value="10000" selected>10秒</option>
          <option value="30000">30秒</option>
          <option value="60000">1分</option>
        </select>
        <button id="refresh-button" class="dashboard-refresh">今すぐ更新</button>
      </div>
    </div>
    
    <div id="last-updated" class="dashboard-status">最終更新: -</div>
    
    <div class="dashboard-summary">
      <div class="summary-card">
        <div class="summary-title">総売上</div>
        <div id="total-sales" class="summary-value">\0</div>
        <div id="sales-change" class="summary-change">前日比: <span class="positive-trend">+0%</span></div>
      </div>
      <div class="summary-card">
        <div class="summary-title">注文数</div>
        <div id="total-orders" class="summary-value">0</div>
        <div id="orders-change" class="summary-change">前日比: <span class="positive-trend">+0%</span></div>
      </div>
      <div class="summary-card">
        <div class="summary-title">平均注文額</div>
        <div id="average-order" class="summary-value">\0</div>
        <div id="average-change" class="summary-change">前日比: <span class="positive-trend">+0%</span></div>
      </div>
      <div class="summary-card">
        <div class="summary-title">新規顧客</div>
        <div id="new-customers" class="summary-value">0</div>
        <div id="customers-change" class="summary-change">前日比: <span class="positive-trend">+0%</span></div>
      </div>
    </div>
    
    <div class="dashboard-grid">
      <div class="dashboard-card">
        <div class="dashboard-card-header">
          <h2 class="dashboard-card-title">時間帯別売上</h2>
        </div>
        <div class="dashboard-chart-container">
          <canvas id="sales-chart"></canvas>
        </div>
      </div>
      
      <div class="dashboard-card">
        <div class="dashboard-card-header">
          <h2 class="dashboard-card-title">カテゴリ別売上</h2>
        </div>
        <div class="dashboard-chart-container">
          <canvas id="category-chart"></canvas>
        </div>
      </div>
      
      <div class="dashboard-card">
        <div class="dashboard-card-header">
          <h2 class="dashboard-card-title">地域別売上</h2>
        </div>
        <div class="dashboard-chart-container">
          <canvas id="region-chart"></canvas>
        </div>
      </div>
      
      <div class="dashboard-card">
        <div class="dashboard-card-header">
          <h2 class="dashboard-card-title">最近の注文</h2>
        </div>
        <table class="dashboard-table">
          <thead>
            <tr>
              <th>注文ID</th>
              <th>顧客名</th>
              <th>商品</th>
              <th>金額</th>
              <th>状態</th>
            </tr>
          </thead>
          <tbody id="recent-orders">
            <tr>
              <td colspan="5">データを読み込み中...</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
  
  <div id="loading" class="dashboard-loading">
    <div class="dashboard-spinner"></div>
  </div>

<script>
 // ダッシュボード用のシンプルなJavaScript
document.addEventListener('DOMContentLoaded', function() {
    // カスタムAPIのエンドポイント(実際の環境に合わせて変更してください)
    const API_ENDPOINT = 'PHP設置用のフォームのURL';
    
    // 更新間隔(ミリ秒)
    let updateInterval = 30000; // デフォルト: 30秒
    let updateTimer = null;
    
    // Chart.jsのグラフインスタンス
    let hourlyChart = null;
    let categoryChart = null;
    let regionChart = null;
    
    // 初期化関数
    function initialize() {
        // 更新間隔の設定
        document.getElementById('interval-select').addEventListener('change', function() {
            updateInterval = parseInt(this.value);
            restartUpdateTimer();
        });
        
        // 初回データ取得
        fetchAllData();
        
        // 定期更新の開始
        startUpdateTimer();
    }
    
    // 更新タイマーの開始
    function startUpdateTimer() {
        updateTimer = setInterval(fetchAllData, updateInterval);
    }
    
    // 更新タイマーの再起動
    function restartUpdateTimer() {
        if (updateTimer) {
            clearInterval(updateTimer);
        }
        startUpdateTimer();
    }
    
    // すべてのデータを取得
    function fetchAllData() {
        fetchSalesSummary();
        fetchHourlySales();
        fetchCategorySales();
        fetchRegionSales();
        fetchRecentOrders();
        
        // 最終更新時刻の表示
        const now = new Date();
        document.getElementById('last-updated').textContent = 
            `最終更新: ${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
    }
    
    // APIリクエストを実行する関数
    function callApi(action, params = {}) {
        showLoading(`${action}-container`);

        const postData = new URLSearchParams({
            action: action,
            ...params
        });

        return fetch(API_ENDPOINT, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
                body: postData.toString()
        })
        .then(response => {
            console.log('HTTPステータス:', response.status);
            return response.text(); // base64文字列が返ってくる想定
        })
        .then(encoded => {
        console.log('受信(base64):', encoded);
        const trimmed = encoded.trim();
        const jsonStr = decodeBase64Utf8(trimmed);

        console.log('デコード後JSON文字列:', jsonStr);
        const data = JSON.parse(jsonStr);

        hideLoading(`${action}-container`);
        return data;
    })
        .catch(error => {
            console.error(`${action}データの取得中にエラーが発生しました:`, error);
            hideLoading(`${action}-container`);
            throw error;
        });
    }


    
    // 売上サマリーデータの取得
    function fetchSalesSummary() {
        showLoading('summary-container');
        
        callApi('getSalesSummary')
            .then(data => {
                updateSalesSummary(data.data.result);
                hideLoading('summary-container');
            })
            .catch(() => {
                // エラーハンドリングは既にcallApi内で行っているため、ここでは何もしない
            });
    }
    
    // 時間帯別売上データの取得
    function fetchHourlySales() {
        showLoading('hourly-chart-container');
        
        callApi('getHourlySales')
            .then(data => {
                updateHourlySalesChart(data.data.result.hourly);
                hideLoading('hourly-chart-container');
            })
            .catch(() => {
                // エラーハンドリングは既にcallApi内で行っているため、ここでは何もしない
            });
    }
    
    // カテゴリ別売上データの取得
    function fetchCategorySales() {
        showLoading('category-chart-container');
        
        callApi('getCategorySales')
            .then(data => {
                updateCategorySalesChart(data.data.result.categories);
                hideLoading('category-chart-container');
            })
            .catch(() => {
                // エラーハンドリングは既にcallApi内で行っているため、ここでは何もしない
            });
    }
    
    // 地域別売上データの取得
    function fetchRegionSales() {
        showLoading('region-chart-container');
        
        callApi('getRegionSales')
            .then(data => {
                updateRegionSalesChart(data.data.result.regions);
                hideLoading('region-chart-container');
            })
            .catch(() => {
                // エラーハンドリングは既にcallApi内で行っているため、ここでは何もしない
            });
    }
    
    // 最近の注文データの取得
    function fetchRecentOrders() {
        showLoading('recent-orders-container');
        
        callApi('getRecentOrders')
            .then(data => {
                updateRecentOrdersTable(data.data.result.orders);
                hideLoading('recent-orders-container');
            })
            .catch(() => {
                // エラーハンドリングは既にcallApi内で行っているため、ここでは何もしない
            });
    }
    
    // 売上サマリーの更新
    function updateSalesSummary(data) {
        document.getElementById('total-sales').textContent = formatCurrency(data.totalSales);
        document.getElementById('sales-change').textContent = formatPercentage(data.salesChangePercent);
        document.getElementById('sales-change').className = getChangeClass(data.salesChangePercent);
        
        document.getElementById('total-orders').textContent = data.totalOrders;
        document.getElementById('orders-change').textContent = formatPercentage(data.ordersChangePercent);
        document.getElementById('orders-change').className = getChangeClass(data.ordersChangePercent);
        
        document.getElementById('average-order').textContent = formatCurrency(data.averageOrder);
        document.getElementById('average-change').textContent = formatPercentage(data.averageChangePercent);
        document.getElementById('average-change').className = getChangeClass(data.averageChangePercent);
        
        document.getElementById('new-customers').textContent = data.newCustomers;
        document.getElementById('customers-change').textContent = formatPercentage(data.customersChangePercent);
        document.getElementById('customers-change').className = getChangeClass(data.customersChangePercent);
    }
    
    // 時間帯別売上チャートの更新
    function updateHourlySalesChart(hourlyData) {
        const labels = hourlyData.map(item => `${item.hour}:00`);
        const salesData = hourlyData.map(item => item.sales);
        const ctx = document.getElementById('sales-chart').getContext('2d');
        if (hourlyChart) {
            hourlyChart.data.labels = labels;
            hourlyChart.data.datasets[0].data = salesData;
            hourlyChart.update();
        } else {
            hourlyChart = new Chart(ctx, {
                type: 'line',
                data: {
                    labels: labels,
                    datasets: [{
                        label: '時間帯別売上',
                        data: salesData,
                        borderColor: 'rgba(54, 162, 235, 1)',
                        backgroundColor: 'rgba(54, 162, 235, 0.2)',
                        borderWidth: 2,
                        tension: 0.3,
                        fill: true
                    }]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    scales: {
                        y: {
                            beginAtZero: true,
                            ticks: {
                                callback: function(value) {
                                    return '\u00A5' + value.toLocaleString();
                                }
                            }
                        }
                    }
                }
            });
        }
    }
    
    // カテゴリ別売上チャートの更新
    function updateCategorySalesChart(categoryData) {
        const labels = categoryData.map(item => item.name);
        const salesData = categoryData.map(item => item.sales);
        
        const ctx = document.getElementById('category-chart').getContext('2d');
        
        if (categoryChart) {
            categoryChart.data.labels = labels;
            categoryChart.data.datasets[0].data = salesData;
            categoryChart.update();
        } else {
            categoryChart = new Chart(ctx, {
                type: 'bar',
                data: {
                    labels: labels,
                    datasets: [{
                        label: 'カテゴリ別売上',
                        data: salesData,
                        backgroundColor: [
                            'rgba(255, 99, 132, 0.7)',
                            'rgba(54, 162, 235, 0.7)',
                            'rgba(255, 206, 86, 0.7)',
                            'rgba(75, 192, 192, 0.7)',
                            'rgba(153, 102, 255, 0.7)'
                        ],
                        borderWidth: 1
                    }]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    scales: {
                        y: {
                            beginAtZero: true,
                            ticks: {
                                callback: function(value) {
                                    return '\u00A5' + value.toLocaleString();
                                }
                            }
                        }
                    }
                }
            });
        }
    }
    
    // 地域別売上チャートの更新
    function updateRegionSalesChart(regionData) {
        const labels = regionData.map(item => item.name);
        const salesData = regionData.map(item => item.sales);
        
        const ctx = document.getElementById('region-chart').getContext('2d');
        
        if (regionChart) {
            regionChart.data.labels = labels;
            regionChart.data.datasets[0].data = salesData;
            regionChart.update();
        } else {
            regionChart = new Chart(ctx, {
                type: 'doughnut',
                data: {
                    labels: labels,
                    datasets: [{
                        label: '地域別売上',
                        data: salesData,
                        backgroundColor: [
                            'rgba(255, 99, 132, 0.7)',
                            'rgba(54, 162, 235, 0.7)',
                            'rgba(255, 206, 86, 0.7)',
                            'rgba(75, 192, 192, 0.7)'
                        ],
                        borderWidth: 1
                    }]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false
                }
            });
        }
    }
    
    // 最近の注文テーブルの更新
    function updateRecentOrdersTable(orders) {
        const tableBody = document.getElementById('recent-orders');
        tableBody.innerHTML = '';
        
        orders.forEach(order => {
            const row = document.createElement('tr');
            
            const idCell = document.createElement('td');
            idCell.textContent = order._id;
            row.appendChild(idCell);
            
            const customerCell = document.createElement('td');
            customerCell.textContent = order.customer_name;
            row.appendChild(customerCell);
            
            const productCell = document.createElement('td');
            productCell.textContent = order.product;
            row.appendChild(productCell);
            
            const amountCell = document.createElement('td');
            amountCell.textContent = formatCurrency(order.total_amount);
            row.appendChild(amountCell);
            
            const statusCell = document.createElement('td');
            statusCell.textContent = order.status;
            row.appendChild(statusCell);
            
            tableBody.appendChild(row);
        });
    }
    
    // ローディング表示
    function showLoading(containerId) {
        const container = document.getElementById(containerId);
        if (container) {
            container.classList.add('loading');
        }
    }
    
    // ローディング非表示
    function hideLoading(containerId) {
        const container = document.getElementById(containerId);
        if (container) {
            container.classList.remove('loading');
        }
    }
    
    // 通貨フォーマット
    function formatCurrency(value) {
            const num = Number(value);
        if (isNaN(num)) return '\u00A50';
        return '\u00A5' + Math.floor(num).toLocaleString();
    }
    
    // パーセンテージフォーマット
    function formatPercentage(value) {
        return (value > 0 ? '+' : '') + value + '%';
    }
    
    // 変化率のクラス取得
    function getChangeClass(value) {
        return value > 0 ? 'positive-change' : (value < 0 ? 'negative-change' : 'no-change');
    }
    
    // 日時フォーマット
    function formatDateTime(dateTimeStr) {
        const date = new Date(dateTimeStr);
        return `${date.getFullYear()}/${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
    }
    
    // base64(UTF-8想定)→ 通常のJS文字列
    function decodeBase64Utf8(base64Str) {
        const binary = atob(base64Str);                    // バイナリ文字列
        const len = binary.length;
        const bytes = new Uint8Array(len);
        for (let i = 0; i < len; i++) {
            bytes[i] = binary.charCodeAt(i);              // 0&#12316;255の数値配列にする
        }
        return new TextDecoder('utf-8').decode(bytes);    // UTF-8としてデコード
    }
    
    // 初期化実行
    initialize();
});
</script>
</body>
</html>
【編集箇所】HTMLソース内>JavaScript変更箇所について

重要: API_ENDPOINTには、PHP設置用のフォームのURLを設定します。

    // カスタムAPIのエンドポイント(実際の環境に合わせて変更してください)
    const API_ENDPOINT = 'PHP設置用のフォームのURL';
    
    // 更新間隔(ミリ秒)
    let updateInterval = 30000; // デフォルト: 30秒
PHP設置用のフォーム(モックデータ PHP)

以下のPHPをPHP設置用のフォームに設定します。モックデータをJSONで返すだけの最小構成です。

<?//<!-- SMP_DYNAMIC_PAGE DISPLAY_ERRORS=ON NAME=XXX -->?>
<?php

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    $requestBody = $_POST;
    $action = $requestBody['action'] ?? '';

    $response = [
        'status' => 'success',
        'data' => [
            'success' => true,
            'result'  => null,
            'error'   => null,
        ],
    ];

    switch ($action) {
        // ■アクション: 売上サマリーの取得(画面上部の4つのカード用)
        case 'getSalesSummary':
            $response['data']['result'] = [
                // [左上のカード] 総売上金額
                // 表示ID: total-sales
                'totalSales'             => 1250000,
                
                // [左上のカード] 総売上の変化率
                // 表示ID: sales-change
                'salesChangePercent'     => 12.5,
                
                // [左から2番目のカード] 総注文数
                // 表示ID: total-orders
                'totalOrders'            => 450,
                
                // [左から2番目のカード] 総注文数の変化率
                // 表示ID: orders-change
                'ordersChangePercent'    => 5.2,
                
                // [左から3番目のカード] 平均注文額
                // 表示ID: average-order
                'averageOrder'           => 2777,
                
                // [左から3番目のカード] 平均注文額の変化率
                // 表示ID: average-change
                'averageChangePercent'   => -2.1,
                
                // [右上のカード] 新規顧客数
                // 表示ID: new-customers
                'newCustomers'           => 35,
                
                // [右上のカード] 新規顧客数の変化率
                // 表示ID: customers-change
                'customersChangePercent' => 8.4,
            ];
            break;

        // ■アクション: 時間帯別売上の取得(左側の折れ線グラフ用)
        case 'getHourlySales':
            $response['data']['result'] = [
                'hourly' => [
                    // hour: X軸(時間), sales: Y軸(売上金額)
                    // グラフID: sales-chart
                    ['hour' => 0,  'sales' => 12000],
                    ['hour' => 1,  'sales' => 8000],
                    ['hour' => 2,  'sales' => 5000],
                    ['hour' => 3,  'sales' => 3000],
                    ['hour' => 4,  'sales' => 2000],
                    ['hour' => 5,  'sales' => 4000],
                    ['hour' => 6,  'sales' => 15000],
                    ['hour' => 7,  'sales' => 28000],
                    ['hour' => 8,  'sales' => 45000],
                    ['hour' => 9,  'sales' => 62000],
                    ['hour' => 10, 'sales' => 75000],
                    ['hour' => 11, 'sales' => 88000],
                    ['hour' => 12, 'sales' => 95000],
                    ['hour' => 13, 'sales' => 82000],
                    ['hour' => 14, 'sales' => 78000],
                    ['hour' => 15, 'sales' => 85000],
                    ['hour' => 16, 'sales' => 92000],
                    ['hour' => 17, 'sales' => 105000],
                    ['hour' => 18, 'sales' => 118000],
                    ['hour' => 19, 'sales' => 125000],
                    ['hour' => 20, 'sales' => 110000],
                    ['hour' => 21, 'sales' => 95000],
                    ['hour' => 22, 'sales' => 70000],
                    ['hour' => 23, 'sales' => 45000],
                ],
            ];
            break;

        // ■アクション: カテゴリ別売上の取得(右上の棒グラフ用)
        case 'getCategorySales':
            $response['data']['result'] = [
                'categories' => [
                    // name: カテゴリ名(X軸), sales: 売上金額(Y軸)
                    // グラフID: category-chart
                    ['name' => 'PC・周辺機器',   'sales' => 450000],
                    ['name' => 'スマートフォン',   'sales' => 380000],
                    ['name' => '家電',   'sales' => 210000],
                    ['name' => 'オフィス用品',   'sales' => 150000],
                    ['name' => 'ソフトウェア', 'sales' => 60000],
                ],
            ];
            break;

        // ■アクション: 地域別売上の取得(左下のドーナツチャート用)
        case 'getRegionSales':
            $response['data']['result'] = [
                'regions' => [
                    // name: 地域名(凡例), sales: 売上金額(割合として表示)
                    // グラフID: region-chart
                    ['name' => '関東', 'sales' => 550000],
                    ['name' => '関西', 'sales' => 350000],
                    ['name' => '東海', 'sales' => 200000],
                    ['name' => 'その他', 'sales' => 150000],
                ],
            ];
            break;

        // ■アクション: 最近の注文の取得(画面右下のテーブル用)
        case 'getRecentOrders':
            $response['data']['result'] = [
                'orders' => [
                    // テーブルID: recent-orders
                    // _id: 注文ID, customer_name: 顧客名, product: 商品名, total_amount: 金額, status: ステータス
                    ['_id' => '1001', 'customer_name' => '山田 太郎', 'product' => 'ノートPC',   'total_amount' => 120000, 'status' => '完了'],
                    ['_id' => '1002', 'customer_name' => '佐藤 花子', 'product' => 'ワイヤレスマウス',   'total_amount' =>  3500, 'status' => '発送準備中'],
                    ['_id' => '1003', 'customer_name' => '鈴木 一郎', 'product' => 'モニター 27インチ',   'total_amount' =>  35000, 'status' => '処理中'],
                    ['_id' => '1004', 'customer_name' => '田中 次郎', 'product' => 'キーボード',       'total_amount' =>  12000, 'status' => '完了'],
                    ['_id' => '1005', 'customer_name' => '伊藤 美咲', 'product' => 'USBハブ','total_amount' =>  2800, 'status' => '完了'],
                ],
            ];
            break;

        default:
            $response['status']          = 'error';
            $response['data']['success'] = false;
            $response['data']['error']   = 'Invalid action: ' . $action;
            break;
    }

    // 1. PHP 配列 → JSON文字列
    $json = json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

    if ($json === false) {
        $errorJson = json_encode([
            'status' => 'error',
            'data'   => [
                'success' => false,
                'result'  => null,
                'error'   => 'json_encode failed: ' . json_last_error_msg(),
            ],
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        echo base64_encode($errorJson);
        exit;
    }

    // 2. JSON文字列 → base64文字列
    echo base64_encode($json);
    exit;
}
?>
実装のポイント
1. 定期更新: setIntervalでデータを一定間隔で取得し再描画
2. 可視化: Chart.jsを使い折れ線・棒・円グラフを表示
3. 更新間隔変更: ユーザーが更新間隔を動的に指定可能
4. レスポンシブ: グリッドレイアウトで各画面サイズに対応
5. UX向上: ローディング表示・最終更新時刻の明示

本格実装パターン(DB連携)

PHP設置用のフォーム(モックデータ PHP)でUIを確認できたら、実際のDBデータを取得するPHPへ切り替えます。
本格実装パターンを用いる場合は、モックデータを返しているPHPの形式に合わせて、
環境内のDBをdoselect等で取得して返すように設定してください。

発展的な機能拡張

以下の機能を追加することで、さらに実用的なダッシュボードへ発展させられます。

1. データフィルタリング: 日付範囲や条件指定での絞り込み
2. ダッシュボードレイアウト変更: ユーザー自身が表示項目をカスタマイズ
3. アラート: 売上急変時に通知を表示
4. エクスポート: CSV/PDFなどへのデータ書き出し
5. 予測分析: 過去データを用いた売上予測の表示
解決しない場合はこちら コンテンツに関しての
要望はこちら