Widget Statistik (Stats Widgets)
Widget statistik (stats widget) adalah sederet angka: jumlah, total, rata-rata, yang masing-masing dapat dilengkapi dengan ikon, warna, tren, sparkline (grafik garis mini), dan tautan opsional. Anda menggunakannya ketika jawabannya adalah sebuah angka — berapa banyak pengguna, berapa banyak pendapatan, berapa banyak pekerjaan yang menunggu — dan pembaca perlu melihatnya sekilas alih-alih sebagai daftar yang harus mereka baca.
Contoh minimal yang berfungsi
php artisan make:panel-widget UserStats --panel=Admin --type=stats2
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Widgets;
use App\Models\User;
use PandaPanel\Widgets\StatsWidget;
use PandaPanel\Widgets\Support\Stat;
final class UserStats extends StatsWidget
{
/**
* @return list<Stat>
*/
public function stats(): array
{
return [
Stat::make('Total users', User::query()->count()),
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Dengan panel yang secara otomatis mendeteksi (discovering) app/Panels/Admin/Widgets, widget tersebut akan langsung dirender (ditampilkan) di dasbor.
Kelas (The class)
PandaPanel\Widgets\StatsWidget merupakan turunan (extends) dari PandaPanel\Widgets\Widget dan menambahkan tepat satu abstract method.
public static function type(): WidgetType // WidgetType::Stats
abstract public function stats(): array // list<Stat>
public function data(): array // ['stats' => list<array>]2
3
4
Semua hal lainnya — $sort, $columnSpan, $lazy, $heading, $description, $pollingInterval, canView(), filterSchema() — berasal dari Widget dan dijelaskan di Gambaran Umum (Overview). StatsWidget tidak menimpa (override) $columnSpan, sehingga ia mewarisi nilai bawaan dasar yaitu 1. Sederet tiga atau empat angka biasanya membutuhkan lebih dari itu:
protected static int|string|array $columnSpan = ['default' => 1, 'md' => 2, 'lg' => 3, 'xl' => 4];2
Angka-angka di dalam widget mengatur tata letaknya sendiri dalam grid yang menyesuaikan secara otomatis (auto-fitting grid) dengan lebar minimum 240px per kartu (card). Jadi, rentang (span) ini mengontrol seberapa besar porsi grid halaman yang ditempati oleh keseluruhan baris, bukan mengontrol berapa banyak angka yang berdampingan satu sama lain.
Stat
PandaPanel\Widgets\Support\Stat adalah value object yang bersifat final readonly. Setiap fluent method akan mengembalikan instance baru, sehingga sebuah stat tidak dapat diubah setelah diserahkan oleh widget.
public function __construct(
public string $label,
public string|int|float $value,
public ?string $description = null,
public ?string $icon = null,
public StatColor $color = StatColor::Default,
public ?array $trend = null,
public array $chart = [],
public ?string $url = null,
public ?string $prefix = null,
public ?string $suffix = null,
public ?int $decimals = null,
) {}2
3
4
5
6
7
8
9
10
11
12
13
14
Anda hampir selalu akan membangunnya menggunakan make() sebagai gantinya.
make()
public static function make(string $label, string\vert{}int\vert{}float$value): self2
use PandaPanel\Widgets\Support\Stat;
Stat::make('Total users', 1_204);
Stat::make('Uptime', '99.9%'); // string dibiarkan sama persis seperti yang tertulis2
3
4
5
description()
public function description(string $description): self2
Satu baris konteks di bawah angka, dipisahkan oleh garis dan sebuah titik berwarna.
Stat::make('New this month', 42)->description('September 2026');2
icon()
public function icon(string $icon): self2
Sebuah kunci ikon (icon key), yang diselesaikan (resolved) melalui registri ikon milik panel — registri yang sama dengan yang digunakan oleh navigasi dan aksi (actions). Kunci yang tidak ada di dalam registri tidak akan merender ikon apa pun alih-alih melempar eror (throwing).
Stat::make('Verified', 980)->icon('shield');2
Jalankan php artisan panel:icons setelah menggunakan kunci yang belum dimiliki oleh registri. Lihat Ikon (Icons).
color()
public function color(StatColor $color): self2
PandaPanel\Widgets\Enums\StatColor bersifat tertutup (closed), karena sisi frontend memetakan setiap kasus langsung ke kelas-kelas Tailwind secara harfiah; nama warna yang bebas (free-form) tidak akan dikompilasi menjadi apa pun.
| Kasus (Case) | Nilai (Value) | Dirender sebagai |
|---|---|---|
StatColor::Default | 'default' | foreground / muted |
StatColor::Success | 'success' | emerald |
StatColor::Warning | 'warning' | amber |
StatColor::Danger | 'danger' | red |
StatColor::Info | 'info' | sky |
use PandaPanel\Widgets\Enums\StatColor;
Stat::make('Failed jobs', 3)->icon('circle-alert')->color(StatColor::Danger);2
3
4
Warna ini akan mewarnai ikon, latar belakangnya, pendaran cahaya lembut (soft ambient glow) di belakang kartu, dan titik deskripsinya.
trend()
/** @param 'up'|'down'|'neutral' $direction */
public function trend(string $direction, float$value): self2
3
Merender sebuah lencana (badge) di sebelah angka: sebuah panah, nilainya dengan tanda % yang ditambahkan oleh perender, dan kata Increased (Meningkat), Decreased (Menurun), atau Unchanged (Tidak Berubah).
Stat::make('Revenue', 12_045)->trend('up', 12.4); // "↗ 12.4% Increased"2
Arah (direction) menentukan warnanya, bukan tanda dari nilainya — tren turun (down) akan berwarna merah baik angkanya 12.4 maupun -12.4. Masukkan nilai besarannya (magnitude) dan tentukan ke arah mana pergerakannya.
chart()
/** @param list<int|float> $values */
public function chart(array $values): self2
3
Sebuah sparkline yang digambar di bawah angka sebagai path SVG sebaris (inline). Ia membutuhkan setidaknya dua nilai; satu nilai atau tidak ada nilai sama sekali tidak akan menggambar apa-apa.
Stat::make('Sign-ups', 412)->chart([4, 9, 7, 12, 18, 21]);2
Ini adalah dekorasi pada angka, bukan grafik di mana orang bisa membaca nilainya — tidak ada sumbu (axes), tidak ada label, dan tidak ada tooltip. Ketika pembaca perlu membaca nilai yang spesifik, gunakan widget grafik (chart widget).
Hitung datanya dalam satu kueri (query). Menjalankan enam kueri hanya untuk sebuah sparkline bukanlah pertukaran (trade-off) yang sepadan:
use App\Models\User;
use Illuminate\Support\Facades\Date;
$start = Date::now()->startOfMonth()->subMonths(5);
$rows = User::query()
->where('created_at', '>=', $start)
->get(['created_at'])
->groupBy(static fn (User $user): string =>$user->created_at?->format('Y-m') ?? '')
->map->count();2
3
4
5
6
7
8
9
10
11
url()
public function url(string $url): self2
Menjadikan seluruh kartu sebagai sebuah tautan (link). Sisi frontend akan merendernya sebagai Link Inertia, sehingga ini menjadi navigasi panel biasa dan halaman tujuan akan mengotorisasi dirinya sendiri saat diikuti — sebuah stat yang menautkan ke resource yang tidak boleh dilihat pengguna akan menghasilkan error 403 di tujuan, bukan kebocoran keamanan (leak) di kartu tersebut.
use App\Panels\Admin\Resources\Users\UserResource;
Stat::make('Total users', User::query()->count())->url(UserResource::url());2
3
4
Bangun URL di server. Resource::url() dan Page::url() keduanya menghasilkan path yang relatif terhadap panel. Lihat URL Resource (Resource URLs).
format()
public function format(?string $prefix = null, ?string $suffix = null, ?int $decimals = null): self2
Atribut apa yang dikenakan angka tersebut dan seberapa presisi angka itu ditulis. Pemformatan terjadi di server karena angka adalah sebuah nilai dan sekaligus bagaimana cara ia seharusnya dibaca: 1,204, £1,204, dan 1,204 ms adalah tiga pernyataan yang berbeda.
Stat::make('Revenue', 12045.5)->format(prefix: '£', decimals: 2); // "£12,045.50"
Stat::make('Latency', 187)->format(suffix: ' ms'); // "187 ms"
Stat::make('Conversion', 0.0731)->format(suffix: '%', decimals: 1); // "0.1%"2
3
4
Setiap argumen bersifat independen; memasukkan hanya satu argumen akan membiarkan argumen lainnya tetap utuh.
display()
public function display(): string2
Angka sebagaimana ia akan dibaca. Dipanggil secara otomatis untuk Anda selama proses serialisasi (serialization); panggil metode ini sendiri hanya saat melakukan pengujian (tests).
| Nilai (Value) | decimals | Hasil (Result) |
|---|---|---|
int | tidak diisi | number_format($value, 0) |
float | tidak diisi | number_format($value, 2) |
int atau float | diisi | number_format($value, $decimals) |
string | apa pun | dikembalikan tanpa perubahan, prefix dan suffix diabaikan |
Stat::make('Revenue', 1204.5)->format(prefix: '£', decimals: 2)->display(); // '£1,204.50'
Stat::make('Uptime', '99.9%')->display(); // '99.9%'2
3
Sebuah widget yang telah memformat nilainya sendiri berarti sudah menyatakan apa yang diinginkannya, sehingga objek ini tidak akan menebak-nebaknya lagi (second-guess).
toArray()
public function toArray(): array2
[
'label' => 'Revenue',
'value' => 1204.5, // nilai mentah (raw), untuk hal-hal yang membutuhkan komputasi
'display' => '£1,204.50', // nilai yang akan digambar
'description' => null,
'icon' => 'receipt',
'color' => 'success',
'trend' => ['direction' => 'up', 'value' => 12.4], // atau null
'chart' => [4, 9, 7], // [] jika tidak ada
'url' => '/admin/orders', // atau null
]2
3
4
5
6
7
8
9
10
11
12
prefix, suffix, dan decimals tidak disertakan di dalam muatan (payload). Ketiganya hanya ada untuk menghasilkan output display dan tidak ada satupun hal lain yang membacanya.
Contoh lengkap (A full example)
Ini adalah isi dari examples/app/Panels/Admin/Widgets/UserStats.php, yang dipangkas menjadi bagian-bagian yang penting saja:
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Widgets;
use App\Models\User;
use App\Panels\Admin\Resources\Users\UserResource;
use Illuminate\Support\Facades\Date;
use PandaPanel\Widgets\Enums\StatColor;
use PandaPanel\Widgets\StatsWidget;
use PandaPanel\Widgets\Support\Stat;
final class UserStats extends StatsWidget
{
protected static int $sort = 10;
protected static int|string|array $columnSpan = ['default' => 1, 'md' => 2, 'lg' => 3, 'xl' => 4];
protected static ?int $pollingInterval = 60;
/**
* @return list<Stat>
*/
public function stats(): array
{
$startOfMonth = Date::now()->startOfMonth();
return [
Stat::make('Total users', User::query()->count())
->color(StatColor::Info)
->icon('users')
->url(UserResource::url()),
Stat::make('Verified', User::query()->whereNotNull('email_verified_at')->count())
->icon('shield')
->color(StatColor::Success)
->description('Confirmed email address'),
Stat::make('New this month', User::query()->where('created_at', '>=', $startOfMonth)->count())
->icon('user')
->color(StatColor::Info)
->description($startOfMonth->format('F Y'))
->chart($this->signUpsPerMonth()),
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Tiga angka, tiga agregat count(*), ditambah satu kueri yang dikelompokkan (grouped query) untuk sparkline. Pengujian (test) bawaan dari paket ini secara khusus menegaskan (asserts) jumlah kueri tersebut, karena widget statistik yang me-hydrate kumpulan data (collections) hanya untuk menghitungnya adalah alasan paling umum mengapa dasbor menjadi halaman paling lambat dalam sebuah aplikasi.
Statistik dengan Filter (Filtered stats)
stats() dapat membaca filter milik widget ataupun filter dari halaman melalui pemanggilan $this->filter():
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\FormSchema;
public function filterSchema(): FormSchema
{
return FormSchema::make()->schema([
Select::make('window')
->options(['7' => 'Last 7 days', '30' => 'Last 30 days'])
->default('30'),
]);
}
public function stats(): array
{
$days = (int)$this->filter('window', 30);
return [
Stat::make("Sign-ups, {$days}d", User::query()
->where('created_at', '>=', now()->subDays($days))
->count()),
];
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Lihat Filter (Filters).
Catatan (Notes)
stats()dapat mengembalikan array kosong. Widget akan tetap merender judul dan filternya; sementara grid di bawahnya hanya akan kosong begitu saja.Statbersifat tidak dapat diubah (immutable). Memanggil$stat->icon('users');saja tidak akan melakukan apa pun — Anda harus menampung nilai kembaliannya.- Sparkline menskalakan nilai minimum dan maksimumnya sendiri, sehingga grafik ini menunjukkan bentuk (shape), bukan besaran absolut (magnitude). Dua sparkline yang berdampingan tidak dapat dibandingkan secara langsung.
- Sebuah stat yang memiliki
url()akan menjadi tautan Inertia mirip elemen<a>yang menutupi seluruh kartu, sehingga tidak ada elemen lain di dalamnya yang dapat diklik secara independen. - Gunakan agregat.
User::query()->count()adalah satu kueri;User::all()->count()akan memuat seluruh tabel.