Hỗ trợ đồ án, khóa luận Web, App, AI - Uy tín, chất lượng!
Hỗ trợ đồ án, khóa luận Web, App, AI - Uy tín, chất lượng!
Trang Chủ Hướng Dẫn Đồ Án Tạo biểu đồ thống kê bằng Chart.js trong PHP
Hướng Dẫn Đồ ÁnLập Trình Web

Tạo biểu đồ thống kê bằng Chart.js trong PHP

Chia sẻ
Chia sẻ

Khi hệ thống của bạn bắt đầu có dữ liệu giao dịch, người dùng, doanh thu… việc trình bày thông tin qua biểu đồ sẽ giúp ra quyết định nhanh và chính xác hơn so với các bảng số liệu đơn thuần. Chart.js là thư viện front-end nhẹ, đẹp và dễ dùng, rất phù hợp để trực quan hóa dữ liệu từ back-end PHP. Trong kiến trúc điển hình, PHP sẽ đọc dữ liệu từ MySQL (qua PDO) và trả về JSON, còn Chart.js trên trình duyệt sẽ gọi API đó để dựng biểu đồ.

Để biến tính năng thống kê thành một phần mạch lạc của sản phẩm, bạn nên tái sử dụng các mô-đun đã làm từ trước: truy vấn DB qua Kết nối MySQL bằng PDO trong PHP, phân trang danh sách dữ liệu bằng LIMIT/OFFSET, bảo mật người dùng với password_hash/password_verify, xác thực phiên đăng nhập qua Session trong PHP và gửi email cảnh báo/điểm báo qua PHPMailer. Bộ nền tảng đó giúp bạn vừa an toàn, vừa mượt trong trải nghiệm, còn Chart.js đảm nhiệm phần hiển thị thông minh ở tầng giao diện.

Kiến trúc tổng quan: PHP (API) + Chart.js (UI)

Trước khi viết code, hãy thống nhất luồng dữ liệu: (1) PHP truy vấn MySQL và xuất JSON từ endpoint /chart_data.php; (2) Chart.js trong trang index.php gọi fetch('/chart_data.php'), nhận JSON và xây dựng biểu đồ; (3) có thể thêm tham số lọc (thời gian, danh mục) để linh hoạt. Cách chia tầng này giúp UI/UX nhanh nhạy và backend dễ test.

Bên cạnh đó, hãy tách riêng cấu hình DB (file db.php), endpoint dữ liệu (file chart_data.php) và trang hiển thị (file index.php). Tư duy module hóa làm cho dự án dễ mở rộng (thêm biểu đồ cột, tròn, stacked…) mà không “đụng” mạnh vào những phần khác.

Chuẩn bị dữ liệu MySQL mẫu

Để có dữ liệu kiểm thử, ta tạo bảng orders lưu doanh thu theo ngày. Trong thực tế, bạn có thể thay bằng bảng giao dịch, đăng ký mới, hoặc sự kiện hệ thống. Việc chuẩn hóa dữ liệu theo ngày (DATE) giúp tổng hợp theo ngày/tuần/tháng rất thuận tiện, đồng thời tránh sai số về timezone khi xử lý ở backend.

Bạn nên lưu số tiền dưới dạng INT (đơn vị nhỏ nhất, ví dụ đồng) để tránh sai số số thực. Khi hiển thị, ta định dạng lại (phân tách hàng nghìn, thêm “đ”). Dưới đây là skeleton SQL để tạo nhanh dữ liệu demo:

-- schema.sql
CREATE TABLE IF NOT EXISTS orders (
  id INT AUTO_INCREMENT PRIMARY KEY,
  order_date DATE NOT NULL,
  amount INT NOT NULL DEFAULT 0, -- lưu đơn vị đồng
  category VARCHAR(50) NOT NULL DEFAULT 'general'
);

-- seed data: 12 ngày gần nhất cho 3 danh mục
INSERT INTO orders (order_date, amount, category) VALUES
('2025-08-16', 1500000, 'apparel'),
('2025-08-17', 2350000, 'apparel'),
('2025-08-18', 1200000, 'electronics'),
('2025-08-19', 3400000, 'apparel'),
('2025-08-20', 2100000, 'home'),
('2025-08-21', 2800000, 'electronics'),
('2025-08-22', 900000,  'home'),
('2025-08-23', 1950000, 'electronics'),
('2025-08-24', 3200000, 'apparel'),
('2025-08-25', 2600000, 'home'),
('2025-08-26', 4100000, 'electronics'),
('2025-08-27', 3700000, 'home');

Kết nối PDO và viết API trả JSON

Tách cấu hình DB vào db.php giúp tái sử dụng và dễ quản lý. Đặt chế độ ERRMODE_EXCEPTION để surfacing lỗi rõ ràng, và charset=utf8mb4 để không lỗi tiếng Việt/emoji. Đảm bảo user DB chỉ có quyền cần thiết (nguyên tắc least privilege).

Sau khi có kết nối, endpoint chart_data.php sẽ nhận (tùy chọn) tham số start, end, category để lọc, rồi trả JSON có mảng labels (ngày) và datasets (doanh thu). Đây là định dạng Chart.js dễ tiêu thụ:

<?php
// db.php
function pdo(): PDO {
    static $pdo = null;
    if ($pdo === null) {
        $pdo = new PDO(
            'mysql:host=localhost;dbname=analytics;charset=utf8mb4',
            'root',
            '',
            [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
        );
    }
    return $pdo;
}
<?php
// chart_data.php
require __DIR__ . '/db.php';

$start = $_GET['start'] ?? null;   // YYYY-MM-DD
$end   = $_GET['end']   ?? null;   // YYYY-MM-DD
$cat   = $_GET['category'] ?? null;

$sql = "SELECT order_date, SUM(amount) AS total_amount
        FROM orders
        WHERE 1=1";
$params = [];

if ($start) { $sql .= " AND order_date >= :start"; $params[':start'] = $start; }
if ($end)   { $sql .= " AND order_date <= :end";   $params[':end']   = $end; }
if ($cat)   { $sql .= " AND category = :cat";      $params[':cat']   = $cat; }

$sql .= " GROUP BY order_date ORDER BY order_date ASC";

$stmt = pdo()->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

// Chuyển dữ liệu thành format Chart.js
$labels = [];
$data   = [];
foreach ($rows as $r) {
    $labels[] = $r['order_date'];
    $data[]   = (int)$r['total_amount'];
}

header('Content-Type: application/json; charset=utf-8');
echo json_encode([
    'labels' => $labels,
    'datasets' => [
        [
            'label' => 'Doanh thu (đ)',
            'data'  => $data
        ]
    ]
]);

Tạo trang HTML + Chart.js (Line chart theo ngày)

Tầng UI chỉ cần một file index.php với <canvas> để Chart.js vẽ biểu đồ. Ta dùng CDN Chart.js cho nhanh. Trang sẽ fetch('/chart_data.php'), sau đó khởi tạo chart với type: 'line'. Với dữ liệu theo ngày dạng YYYY-MM-DD, ta có thể dùng axis danh mục (category) hoặc time scale (có adapter). Ở bản cơ bản, dùng category axis là đủ.

Trong thực tế, bạn có thể tích hợp trang này vào khu vực dashboard sau khi người dùng đăng nhập (kết hợp Session trong PHP) và phân quyền bằng middleware đơn giản (tham khảo CRUD PHP):

<?php // index.php ?>
<!doctype html>
<html lang="vi">
<head>
  <meta charset="utf-8">
  <title>Thống kê doanh thu - Chart.js + PHP</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- Chart.js CDN -->
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
  <style>
    body{font:16px/1.5 system-ui,Segoe UI,Roboto,Helvetica,Arial}
    .wrap{max-width:1000px;margin:24px auto;padding:0 12px}
    canvas{max-height:420px}
  </style>
</head>
<body>
<div class="wrap">
  <h1>Thống kê doanh thu theo ngày</h1>
  <canvas id="lineChart"></canvas>
</div>

<script>
async function render() {
  const res = await fetch('chart_data.php');
  const json = await res.json();

  const ctx = document.getElementById('lineChart').getContext('2d');
  new Chart(ctx, {
    type: 'line',
    data: json,
    options: {
      responsive: true,
      plugins: {
        legend: { display: true },
        tooltip: {
          callbacks: {
            label: (ctx) => {
              const v = ctx.parsed.y || 0;
              return ctx.dataset.label + ': ' + v.toLocaleString('vi-VN') + 'đ';
            }
          }
        }
      },
      scales: {
        y: {
          ticks: {
            callback: (v) => v.toLocaleString('vi-VN') + 'đ'
          }
        }
      }
    }
  });
}
render();
</script>
</body>
</html>

Biểu đồ cột so sánh theo tháng (grouped/stacked)

Ngoài đường (line), biểu đồ cột (bar) rất hợp để so sánh theo tháng hoặc theo danh mục. Ý tưởng: back-end trả về nhiều datasets (mỗi danh mục là một dataset), hoặc gộp theo tháng để bớt nhiễu. Với grouped bar, mỗi nhãn (tháng) sẽ có nhiều cột cạnh nhau; còn stacked thì chồng lên nhau để xem tổng.

Phần này bạn có thể dùng view khác hoặc thêm một <canvas> thứ hai trong index.php. Để ngắn gọn, ví dụ dưới giả sử chart_data.php?group=month trả dữ liệu đã gộp theo YYYY-MM với nhiều danh mục:

<?php
// chart_data.php (biến thể gộp theo tháng & nhiều danh mục) - rút gọn ý tưởng
// ... kết nối như trên
$group = $_GET['group'] ?? 'day';
if ($group === 'month') {
  $sql = "SELECT DATE_FORMAT(order_date, '%Y-%m') AS label, category, SUM(amount) total
          FROM orders WHERE 1=1";
  // ... append điều kiện & bind params
  $sql .= " GROUP BY label, category ORDER BY label ASC";
  // ... fetch $rows = [label, category, total]
  // map -> labels unique, datasets per category
}
<!-- Thêm vào index.php -->
<div class="wrap">
  <h2>Doanh thu theo tháng (grouped bar)</h2>
  <canvas id="barChart"></canvas>
</div>
<script>
async function renderBar() {
  const res = await fetch('chart_data_month_grouped.php'); // endpoint gộp sẵn
  const json = await res.json(); // { labels: ['2025-08','2025-09'], datasets:[{label:'apparel',data:[...]},...] }

  new Chart(document.getElementById('barChart').getContext('2d'), {
    type: 'bar',
    data: json,
    options: {
      responsive: true,
      plugins: { legend: { position: 'top' } },
      scales: {
        y: {
          stacked: false, // true nếu muốn stacked
          ticks: { callback: (v)=> v.toLocaleString('vi-VN') + 'đ' }
        }
      }
    }
  });
}
renderBar();
</script>

Biểu đồ tròn/phân bổ theo danh mục

Khi cần trực quan hóa tỷ trọng (ví dụ tỷ lệ doanh thu theo danh mục), doughnut/pie phát huy tác dụng. Back-end chỉ cần trả tổng theo danh mục cho một khoảng thời gian: nhãn là tên danh mục, dữ liệu là tổng tiền. Với kiểu biểu đồ này, bạn dễ thấy danh mục nào đang “ăn điểm”.

Hãy tạo endpoint chart_share.php trả { labels: [...], datasets: [{ data: [...] }] }, rồi dùng type: 'doughnut':

<?php
// chart_share.php (rút gọn)
require __DIR__ . '/db.php';
$start = $_GET['start'] ?? null;
$end   = $_GET['end']   ?? null;

$sql = "SELECT category, SUM(amount) total FROM orders WHERE 1=1";
$params = [];
if ($start) { $sql .= " AND order_date >= :start"; $params[':start'] = $start; }
if ($end)   { $sql .= " AND order_date <= :end";   $params[':end']   = $end; }
$sql .= " GROUP BY category ORDER BY total DESC";

$stmt = pdo()->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

$labels = array_column($rows, 'category');
$data   = array_map('intval', array_column($rows, 'total'));

header('Content-Type: application/json; charset=utf-8');
echo json_encode(['labels'=>$labels, 'datasets'=>[['data'=>$data]]]);
<!-- Thêm vào index.php (hoặc trong block HTML của WP) -->
<div class="wrap">
  <h2>Tỷ trọng doanh thu theo danh mục</h2>
  <canvas id="pieChart"></canvas>
</div>

<script>
document.addEventListener('DOMContentLoaded', function () {
  (function () {
    // Tự nạp Chart.js nếu chưa có
    function ensureChartJs(cb) {
      if (window.Chart) return cb();
      var s = document.createElement('script');
      s.src = "https://cdn.jsdelivr.net/npm/chart.js";
      s.onload = cb;
      s.onerror = function(){ console.error('Không tải được Chart.js'); };
      document.head.appendChild(s);
    }

    ensureChartJs(function () {
      fetch('chart_share.php', { cache: 'no-store' })
        .then(function (res) { return res.json(); })
        .then(function (json) {
          var el = document.getElementById('pieChart');
          if (!el) return console.error('Không tìm thấy #pieChart');
          var ctx = el.getContext('2d');

          new Chart(ctx, {
            type: 'doughnut',
            data: json,
            options: {
              plugins: {
                tooltip: {
                  callbacks: {
                    label: function (ctx) {
                      var dataArr = ctx.chart.data.datasets[0].data || [];
                      var total = dataArr.reduce(function (a, b) { return a + b; }, 0);
                      var v = ctx.parsed;
                      var pct = total ? (v * 100 / total).toFixed(1) : 0;
                      var lbl = ctx.label != null ? ctx.label : '';
                      try {
                        return lbl + ': ' + Number(v).toLocaleString('vi-VN') + 'đ (' + pct + '%)';
                      } catch (e) {
                        return lbl + ': ' + v + 'đ (' + pct + '%)';
                      }
                    }
                  }
                }
              }
            }
          });
        })
        .catch(function (err) {
          console.error('Lỗi tải dữ liệu biểu đồ:', err);
        });
    });
  })();
});
</script>

Trục thời gian & adapter (time scale)

Khi cần hiển thị dải ngày dài (nhiều tháng), bạn nên dùng time scale để Chart.js tự format nhãn. Chart.js 4 dùng cơ chế adapter; cách nhanh nhất là thêm date-fns adapter. Điều này giúp trục X gọn gàng hơn và bạn có thể đặt time.unit (day, month…) để giảm mật độ nhãn.

Trước khi dùng, thêm adapter CDN:

<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>

Và thay options:

scales: {
  x: {
    type: 'time',
    time: { unit: 'day' }
  },
  y: { ticks: { callback: v => v.toLocaleString('vi-VN') + 'đ' } }
}

Tùy biến tooltip/legend & định dạng tiền tệ

Trải nghiệm đọc số liệu phụ thuộc rất nhiều vào tooltipticks. Bạn đã thấy ở ví dụ, ta dùng toLocaleString('vi-VN') để format tiền. Có thể thêm tiền tố/hậu tố (đ, VNĐ), rút gọn theo đơn vị (K, M) với một hàm util nhỏ. Legend có thể đặt position: 'bottom' để tiết kiệm không gian ở chart dọc.

Ví dụ formatter rút gọn:

const fmtCompact = (n) => {
  if (n >= 1_000_000) return (n/1_000_000).toFixed(1) + 'M';
  if (n >= 1_000)     return (n/1_000).toFixed(1) + 'K';
  return n.toString();
};

Bộ lọc thời gian/danh mục (UI) → tham số API

Lọc theo khoảng ngàydanh mục nên thực hiện ở server để tránh tải dữ liệu dư thừa. Bạn có thể thêm form ở index.php gửi query string vào API; phía PHP bind tham số an toàn qua PDO (không nối chuỗi bừa bãi). Khi người dùng đổi filter, chỉ cần gọi lại fetch và cập nhật chart (chart.data = ...; chart.update()).

Ví dụ form rút gọn:

<form id="f" class="wrap" onsubmit="ev(event)">
  <label>Từ ngày <input type="date" name="start"></label>
  <label>Đến ngày <input type="date" name="end"></label>
  <label>Danh mục
    <select name="category">
      <option value="">Tất cả</option>
      <option value="apparel">apparel</option>
      <option value="electronics">electronics</option>
      <option value="home">home</option>
    </select>
  </label>
  <button>Lọc</button>
</form>
<script>
let chart;
async function loadWithParams(qs) {
  const res = await fetch('chart_data.php' + (qs ? '?' + qs : ''));
  const data = await res.json();
  const ctx = document.getElementById('lineChart').getContext('2d');
  if (chart) { chart.data = data; chart.update(); }
  else {
    chart = new Chart(ctx, { type:'line', data, options:{ responsive:true }});
  }
}
function ev(e) {
  e.preventDefault();
  const fd = new FormData(e.target);
  const qs = new URLSearchParams(fd).toString();
  loadWithParams(qs);
}
loadWithParams('');
</script>

Bảo mật & hiệu năng

Về bảo mật, không phơi bày bảng/field nội bộ ra API; chỉ trả đúng dữ liệu thống kê đã xử lý. Validate/whitelist tham số (start, end, category), tránh SQL injection (đã dùng prepared statement). Nếu biểu đồ là khu vực quản trị, bắt buộc kiểm tra đăng nhập  và phân quyền.

Về hiệu năng, với dữ liệu lớn, hãy tổng hợp theo ngày/tuần/tháng bằng GROUP BY; cân nhắc làm materialized view hoặc bảng tổng hợp chạy theo lịch. Phía front-end, đừng vẽ quá nhiều điểm; nếu chuỗi rất dài, hãy down-sample hoặc phân trang thời gian. Bật HTTP caching hợp lý cho endpoint không theo thời gian thực.

Kết luận

Bạn vừa thiết lập một pipeline trực quan hóa dữ liệu chuẩn: PHP (PDO) → JSON API → Chart.js. Với vài endpoint gọn và một trang HTML, bạn đã có line/bar/doughnut đủ dùng cho dashboard thực chiến. Tiếp theo, hãy chuẩn hóa lớp truy vấn (repository), thêm cache, log, và kiểm thử. Song song, duy trì nguyên tắc: lọc ở server, định dạng ở client, bảo mật theo vai trò—bạn sẽ có một hệ thống thống kê vừa đẹp vừa vững.

Tham Gia Nhóm – Hỗ Trợ Lập Trình

Hỗ Trợ Đồ Án – ThueDoAn.vn

Liên quan
Hướng Dẫn Đồ Án

Checklist 10 bước kiểm tra cuối cùng trước khi nhấn nút nộp đồ án

Thời điểm chuẩn bị nhấn nút nộp đồ án luôn mang lại...

Hướng Dẫn Đồ Án

Tuyệt chiêu đối phó với những giảng viên hướng dẫn ‘khó tính’ nhất

Giai đoạn làm đồ án tốt nghiệp luôn đi kèm với vô...

Hướng Dẫn Đồ Án

Tại sao việc quản lý Source Code bằng Git lại quan trọng hơn bạn nghĩ?

Trong thế giới lập trình hiện đại, mã nguồn chính là linh...

Hướng Dẫn Đồ Án

Những chức năng ‘thừa’ khiến bạn tốn thời gian mà không được cộng điểm

Làm đồ án tốt nghiệp là một cuộc đua thực thụ với...

Hướng Dẫn Đồ Án

Cách xử lý khi đề tài bị trùng lặp ý tưởng với các khóa trước quá nhiều

Lựa chọn đề tài đồ án luôn là bước khởi đầu đầy...

Hướng Dẫn Đồ Án

Làm sao để giải trình với giảng viên khi code bị nghi ngờ là đi chép?

Trong quá trình thực hiện đồ án, việc tham khảo mã nguồn...

Hướng Dẫn Đồ Án

Cảnh báo: Những nguồn share code ‘rác’ trên mạng cần tuyệt đối tránh

Trong quá trình làm đồ án, việc tìm kiếm sự hỗ trợ...

Hướng Dẫn Đồ Án

Sai lầm khi chọn công nghệ quá khó so với năng lực thực tế của bản thân

Chọn đề tài và công nghệ cho đồ án luôn là bước...

Liên Hệ
Liên hệ để được tư vấn & hỗ trợ đồ án CNTT nhé bạn!