DEV Community

Cover image for Laravel 13 Chart.js Tutorial: Build Dynamic Charts from Database Data
Muhammad Yasir Amin
Muhammad Yasir Amin

Posted on Originally published at yasiramin.dev

Laravel 13 Chart.js Tutorial: Build Dynamic Charts from Database Data

To build a chart from database data in Laravel 13, install Chart.js with npm, create a controller method that returns the data as JSON, fetch it in JavaScript and pass it to new Chart(). This keeps the query on the server and the drawing in the browser. This tutorial builds a monthly sales bar chart from an orders table.

Targets: Laravel 13 (PHP 8.3 to 8.5), Vite and the chart.js npm package.

Step 1: Install Chart.js

npm install chart.js
Enter fullscreen mode Exit fullscreen mode

Step 2: Prepare the data

This example assumes an orders table with a total decimal column and the usual created_at timestamp. Create the model and migration if you do not have them:

php artisan make:model Order -mf
Enter fullscreen mode Exit fullscreen mode
Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->decimal('total', 10, 2);
    $table->timestamps();
});
Enter fullscreen mode Exit fullscreen mode

Add a few rows with a factory or Tinker so the chart has something to show. Use different created_at months.

Step 3: Return JSON from a controller

Group orders by month and sum the totals. The query below works on SQLite, MySQL and MariaDB because it uses a format function that each engine supports under its own name, so the example picks the right expression for the current connection:

<?php

namespace App\Http\Controllers;

use App\Models\Order;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;

class SalesChartController extends Controller
{
    public function __invoke(): JsonResponse
    {
        $monthExpression = DB::connection()->getDriverName() === 'sqlite'
            ? "strftime('%Y-%m', created_at)"
            : "DATE_FORMAT(created_at, '%Y-%m')";

        $rows = Order::query()
            ->selectRaw("$monthExpression as month, SUM(total) as revenue")
            ->where('created_at', '>=', now()->subMonths(11)->startOfMonth())
            ->groupBy('month')
            ->orderBy('month')
            ->get();

        return response()->json([
            'labels' => $rows->pluck('month'),
            'values' => $rows->pluck('revenue')->map(fn ($value) => (float) $value),
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

The expression strings are fixed in the code, so there is no SQL injection risk from user input. If you use PostgreSQL, use to_char(created_at, 'YYYY-MM') instead. The response has two arrays, labels for the x axis and values for the bars.

Step 4: Add the route and the page

use App\Http\Controllers\SalesChartController;
use Illuminate\Support\Facades\Route;

Route::get('/charts/sales', SalesChartController::class)->middleware('auth');
Route::view('/dashboard/sales', 'sales');
Enter fullscreen mode Exit fullscreen mode

Create resources/views/sales.blade.php with a canvas element:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Sales</title>
    @vite(['resources/js/app.js'])
</head>
<body>
    <div style="max-width: 800px; margin: 2rem auto;">
        <canvas id="salesChart" data-url="{{ url('/charts/sales') }}"></canvas>
    </div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Step 5: Draw the chart

Add this to resources/js/app.js:

import Chart from 'chart.js/auto';

const canvas = document.getElementById('salesChart');

if (canvas) {
    fetch(canvas.dataset.url, { headers: { 'Accept': 'application/json' } })
        .then((response) => response.json())
        .then((data) => {
            new Chart(canvas, {
                type: 'bar',
                data: {
                    labels: data.labels,
                    datasets: [{
                        label: 'Revenue',
                        data: data.values,
                        backgroundColor: 'rgba(13, 110, 253, 0.6)',
                        borderRadius: 4,
                    }],
                },
                options: {
                    responsive: true,
                    plugins: { legend: { display: false } },
                    scales: { y: { beginAtZero: true } },
                },
            });
        });
}
Enter fullscreen mode Exit fullscreen mode

The import chart.js/auto registers all chart types and components automatically. It is the easiest choice. For a smaller bundle you can import only the pieces you use and register them yourself, as the Chart.js documentation explains.

How do I change the chart type?

Change type: use 'line' for trends, 'pie' or 'doughnut' for shares of a total, and 'radar' or 'polarArea' for other comparisons. A line chart usually needs tension: 0.3 in the dataset for a smooth curve and fill: false to avoid the filled area.

How do I refresh the chart without reloading the page?

Keep the chart in a variable and update it when new data arrives:

chart.data.labels = data.labels;
chart.data.datasets[0].data = data.values;
chart.update();
Enter fullscreen mode Exit fullscreen mode

Call that code from a timer or a button click that fetches the endpoint again.

How do I check that it works?

  1. Run npm run dev and php artisan serve.
  2. Open /charts/sales in the browser (while logged in) and confirm you get JSON with labels and values.
  3. Open /dashboard/sales and confirm the bars appear.

Common mistakes

  • Chart is not defined: you did not import it in a script that Vite builds.
  • Empty chart: the JSON endpoint returned an error. Check the network tab.
  • Database-specific date functions: DATE_FORMAT does not exist in SQLite and PostgreSQL. Pick the right function per database.
  • Canvas sized by CSS only: put the canvas in a container with a width, and keep responsive: true.
  • Returning decimals as strings: cast revenue to float so Chart.js handles the values as numbers.

FAQ

Can I use the CDN instead of npm?

Yes, with a script tag, but npm with Vite gives you version control and bundling.

Do I need a Laravel package for charts?

No. Chart.js works directly with a JSON endpoint. Packages mainly wrap the same pattern.

How do I show several datasets?

Add more objects to the datasets array, each with its own label, data and color.

References


Originally published on yasiramin.dev.

Top comments (0)