SaaS dashboards visualize key metrics for business decisions. Chart.js with Alpine.js creates interactive, real-time dashboards. At ZIRA Software, we've built analytics dashboards processing millions of data points.
Project Setup
# Install dependencies
npm install chart.js alpinejs
# Or via CDN in blade
<!-- resources/views/layouts/app.blade.php -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
Dashboard Controller
class DashboardController extends Controller
{
public function index()
{
return view('dashboard.index', [
'stats' => $this->getStats(),
'revenueData' => $this->getRevenueData(),
'userGrowth' => $this->getUserGrowth(),
]);
}
private function getStats()
{
return [
'total_revenue' => Order::sum('total'),
'total_users' => User::count(),
'active_subscriptions' => Subscription::active()->count(),
'churn_rate' => $this->calculateChurnRate(),
];
}
private function getRevenueData()
{
return Order::selectRaw('DATE(created_at) as date, SUM(total) as revenue')
->where('created_at', '>=', now()->subDays(30))
->groupBy('date')
->orderBy('date')
->get();
}
private function getUserGrowth()
{
return User::selectRaw('DATE(created_at) as date, COUNT(*) as count')
->where('created_at', '>=', now()->subDays(30))
->groupBy('date')
->orderBy('date')
->get();
}
}
Stats Cards Component
<!-- resources/views/dashboard/partials/stats.blade.php -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-3 rounded-full bg-blue-100 text-blue-600">
<svg class="w-6 h-6"><!-- Icon --></svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-500">Total Revenue</p>
<p class="text-2xl font-semibold text-gray-900">
${{ number_format($stats['total_revenue'], 2) }}
</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100 text-green-600">
<svg class="w-6 h-6"><!-- Icon --></svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-500">Active Users</p>
<p class="text-2xl font-semibold text-gray-900">
{{ number_format($stats['total_users']) }}
</p>
</div>
</div>
</div>
</div>
Revenue Chart with Alpine
<div
x-data="revenueChart()"
x-init="init()"
class="bg-white rounded-lg shadow p-6"
>
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Revenue Overview</h3>
<select
x-model="period"
@change="updateChart()"
class="border rounded px-3 py-1"
>
<option value="7">Last 7 days</option>
<option value="30" selected>Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
</div>
<canvas x-ref="chart" height="300"></canvas>
</div>
<script>
function revenueChart() {
return {
chart: null,
period: '30',
init() {
this.createChart(@json($revenueData));
},
createChart(data) {
const ctx = this.$refs.chart.getContext('2d');
this.chart = new Chart(ctx, {
type: 'line',
data: {
labels: data.map(d => d.date),
datasets: [{
label: 'Revenue',
data: data.map(d => d.revenue),
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
tension: 0.4,
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: (ctx) => `$${ctx.parsed.y.toFixed(2)}`
}
}
},
scales: {
y: {
ticks: {
callback: (value) => `$${value}`
}
}
}
}
});
},
async updateChart() {
const response = await fetch(`/api/dashboard/revenue?period=${this.period}`);
const data = await response.json();
this.chart.data.labels = data.map(d => d.date);
this.chart.data.datasets[0].data = data.map(d => d.revenue);
this.chart.update();
}
}
}
</script>
API Endpoint for Dynamic Data
// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
Route::get('/dashboard/revenue', [DashboardApiController::class, 'revenue']);
Route::get('/dashboard/users', [DashboardApiController::class, 'users']);
});
// app/Http/Controllers/Api/DashboardApiController.php
class DashboardApiController extends Controller
{
public function revenue(Request $request)
{
$days = $request->get('period', 30);
return Order::selectRaw('DATE(created_at) as date, SUM(total) as revenue')
->where('created_at', '>=', now()->subDays($days))
->groupBy('date')
->orderBy('date')
->get();
}
}
Multi-Chart Dashboard
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Revenue Line Chart -->
<div x-data="lineChart('revenue', @json($revenueData))">
<canvas x-ref="canvas"></canvas>
</div>
<!-- User Growth Bar Chart -->
<div x-data="barChart('users', @json($userGrowth))">
<canvas x-ref="canvas"></canvas>
</div>
<!-- Subscription Breakdown Doughnut -->
<div x-data="doughnutChart(@json($subscriptionBreakdown))">
<canvas x-ref="canvas"></canvas>
</div>
<!-- Top Products -->
<div x-data="horizontalBarChart(@json($topProducts))">
<canvas x-ref="canvas"></canvas>
</div>
</div>
Real-Time Updates with Polling
function dashboardPoller() {
return {
stats: @json($stats),
interval: null,
init() {
this.interval = setInterval(() => this.refresh(), 30000);
},
async refresh() {
const response = await fetch('/api/dashboard/stats');
this.stats = await response.json();
// Dispatch event for charts to update
this.$dispatch('stats-updated', this.stats);
},
destroy() {
clearInterval(this.interval);
}
}
}
Conclusion
Laravel with Chart.js and Alpine.js creates powerful, interactive SaaS dashboards. Real-time updates and responsive design ensure effective data visualization.
Building SaaS analytics? Contact ZIRA Software for dashboard development.