Widget Chart
Widget chart menggambar satu atau beberapa series angka terhadap sekumpulan label yang sama. Gunakan widget ini ketika bentuk atau pola data adalah informasi utamanya — misalnya pertumbuhan per bulan, volume per hari, atau perbandingan satu metrik dengan metrik lain — dan satu angka saja tidak cukup menjelaskannya.
Chart digambar oleh inline SVG renderer tanpa dependency yang sudah disertakan dalam package. Data yang dikirim ke browser hanyalah deskripsi chart: label, series, sekumpulan option yang terbatas, dan tinggi chart. Tidak ada library chart tambahan yang diinstal, dan tidak ada konfigurasi yang dikirim sebagai executable behaviour.
Contoh minimal yang dapat langsung digunakan
php artisan make:panel-widget UserGrowth --panel=Admin --type=chart<?php
declare(strict_types=1);
namespace App\Panels\Admin\Widgets;
use PandaPanel\Widgets\ChartWidget;
use PandaPanel\Widgets\Support\ChartSeries;
final class UserGrowth extends ChartWidget
{
protected static ?string $heading = 'Sign-ups';
/**
* @return list<string>
*/
public function labels(): array
{
return ['Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep'];
}
/**
* @return list<ChartSeries>
*/
public function series(): array
{
return [
ChartSeries::make('Sign-ups', [12, 19, 14, 31, 28, 44]),
];
}
}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
Hasilnya adalah bar chart dengan enam titik dan satu series pada dashboard panel.
Class
PandaPanel\Widgets\ChartWidget extends PandaPanel\Widgets\Widget.
| Member | Signature | Default |
|---|---|---|
$columnSpan | protected static int|string|array | ['default' => 1, 'md' => 2, 'lg' => 2, 'xl' => 2] |
$variant | protected static ChartVariant | ChartVariant::Bar |
$maxHeight | protected static int | 220 |
type() | public static function type(): WidgetType | WidgetType::Chart |
labels() | abstract public function labels(): array | — |
series() | abstract public function series(): array | — |
options() | public function options(): ChartOptions | ChartOptions::make() |
data() | public function data(): array | lihat penjelasan di bawah |
Berbeda dari StatsWidget, ChartWidget melakukan override pada $columnSpan. Chart yang dipaksa masuk ke satu kolom dari grid empat kolom akan sulit dibaca, sehingga secara default chart mengambil setengah lebar grid mulai breakpoint md.
use PandaPanel\Widgets\Enums\ChartVariant;
protected static ChartVariant $variant = ChartVariant::Area;
protected static int $maxHeight = 200;2
3
4
5
$maxHeight adalah tinggi area plot dalam pixel. Chart tanpa tinggi yang ditentukan sendiri akan mengikuti tinggi container, dan dashboard yang berisi banyak chart seperti itu dapat berubah menjadi halaman panjang yang sulit dibandingkan secara visual.
labels()
/** @return list<string> */
abstract public function labels(): array2
Satu label digunakan untuk setiap titik dan dibagikan ke semua series. Label digambar di bawah plot dan digunakan sebagai heading tooltip. Jika sebuah series memiliki value lebih banyak daripada jumlah label, titik tambahan tetap digambar tetapi labelnya kosong.
series()
/** @return list<ChartSeries> */
abstract public function series(): array2
options()
public function options(): ChartOptionsOverride method ini jika Anda membutuhkan konfigurasi selain default. Lihat ChartOptions.
data()
public function data(): array[
'variant' => 'area',
'labels' => ['Apr', 'May', 'Jun'],
'series' => [['label' => 'Sign-ups', 'values' => [12, 19, 14], 'color' => 'info']],
'options' => ['legend' => false, 'grid' => true, /* ... */],
'maxHeight' => 200,
]2
3
4
5
6
7
labels() dan series() masing-masing dipanggil satu kali pada setiap render. Jika keduanya berasal dari query yang sama, lakukan memoization agar query tidak dijalankan dua kali — lihat contoh lengkap di bawah.
Variant
PandaPanel\Widgets\Enums\ChartVariant bersifat tertutup karena setiap case dipetakan ke bentuk yang memang diketahui oleh renderer bawaan pada saat compile.
| Case | Value | Digambar sebagai |
|---|---|---|
ChartVariant::Bar | 'bar' | grouped bar, satu group per label |
ChartVariant::Line | 'line' | stroked path untuk setiap series |
ChartVariant::Area | 'area' | stroked path untuk setiap series dengan area di bawahnya terisi |
ChartVariant::Doughnut | 'doughnut' | bar — lihat catatan di bawah |
Area adalah Line dengan filled yang dipaksa aktif; Anda tidak perlu memanggil ChartOptions::filled() lagi untuk variant ini.
Doughnut diterima oleh type system dan tetap diserialisasi, tetapi SVG renderer bawaan belum memiliki kemampuan menggambar arc. Semua variant selain line atau area jatuh ke jalur rendering bar. Jika Anda memerlukan ring atau pie chart yang sebenarnya, gunakan custom Vue widget; itu adalah pilihan yang tepat ketika visual memang membutuhkan implementasi khusus.
ChartSeries
PandaPanel\Widgets\Support\ChartSeries adalah final readonly value object dan bersifat immutable seperti Stat.
public function __construct(
public string $label,
public array $values, // list<int|float>
public StatColor $color = StatColor::Default,
) {}
/** @param list<int|float> $values */
public static function make(string $label, array $values): self
public function color(StatColor $color): self
/** @return array{label: string, values: list<int|float>, color: string} */
public function toArray(): array2
3
4
5
6
7
8
9
10
11
12
13
use PandaPanel\Widgets\Enums\StatColor;
use PandaPanel\Widgets\Support\ChartSeries;
public function series(): array
{
return [
ChartSeries::make('Sign-ups', [12, 19, 14])->color(StatColor::Info),
ChartSeries::make('Cancellations', [2, 4, 1])->color(StatColor::Danger),
];
}2
3
4
5
6
7
8
9
10
Warna berasal dari enum tertutup PandaPanel\Widgets\Enums\StatColor yang sama seperti pada stat — Default, Success, Warning, Danger, Info — lalu dipetakan menjadi literal Tailwind class di frontend. Dua series yang dibiarkan menggunakan Default akan digambar dengan warna yang sama, jadi tentukan warna per series ketika chart memiliki lebih dari satu series.
Value non-finite (NAN, INF) dikeluarkan dari domain axis dan tooltip. Chart yang seluruh series-nya tidak memiliki angka finite akan merender pesan "No data for this period." Nilai tersebut belum dikeluarkan dari path yang digambar, jadi kirimkan angka finite — misalnya cast aggregate nullable menjadi 0 daripada membiarkan null berubah menjadi NAN.
ChartOptions
PandaPanel\Widgets\Support\ChartOptions adalah mutable fluent builder — setiap method mengembalikan $this.
| Method | Signature | Default | Efek |
|---|---|---|---|
make() | public static function make(): self | — | Membuat instance baru dengan default di bawah. |
legend() | public function legend(bool $legend = true): self | true | Menggambar key series di atas plot. |
grid() | public function grid(bool $grid = true): self | true | Menggambar empat garis grid horizontal putus-putus. |
stacked() | public function stacked(bool $stacked = true): self | false | Menumpuk bar menjadi satu kolom per label, bukan berdampingan. |
filled() | public function filled(bool $filled = true): self | false | Mengisi area di bawah line. Otomatis aktif pada ChartVariant::Area. |
curved() | public function curved(bool $curved = true): self | false | Menghaluskan line. Membutuhkan sedikitnya tiga titik. |
labels() | public function labels(bool $labels = true): self | false | Meminta label value pada setiap titik. Lihat catatan di bawah. |
range() | public function range(?float $min, ?float $max): self | null, null | Mengunci value axis. |
format() | public function format(?string $prefix = null, ?string $suffix = null): self | null, null | Menentukan prefix/suffix ketika value ditampilkan. |
toArray() | public function toArray(): array | — | Option yang sudah diserialisasi. |
use PandaPanel\Widgets\Support\ChartOptions;
public function options(): ChartOptions
{
return ChartOptions::make()
->legend(false)
->curved()
->filled();
}2
3
4
5
6
7
8
9
range()
ChartOptions::make()->range(0, 100);
ChartOptions::make()->range(0, null); // pin the floor, let the ceiling follow the data2
Gunakan ini ketika chart harus dibaca terhadap target tertentu, bukan hanya terhadap range datanya sendiri. Axis yang selalu menyesuaikan diri dengan data dapat membuat setiap minggu terlihat memiliki pola yang sama walaupun nilainya berbeda jauh.
Jika kedua batas diberikan, nilai tersebut digunakan apa adanya. Jika hanya satu atau tidak ada batas yang diberikan, renderer menentukan batas yang hilang dari data, tetap menjaga nol terlihat agar bar memiliki baseline yang bermakna, lalu menambahkan ruang sekitar 8% agar titik tertinggi tidak menempel ke tepi atas.
format()
ChartOptions::make()->format(prefix: '£');
ChartOptions::make()->format(suffix: '%');2
Format diterapkan pada value yang ditampilkan di tooltip. Angkanya sendiri diformat menggunakan Intl.NumberFormat dengan maksimal dua digit desimal.
labels()
labels(true) diserialisasi menjadi options.labels, tetapi renderer bawaan saat ini belum menggambar label value pada setiap titik. Value tetap tersedia melalui tooltip yang mengikuti pointer atau keyboard focus antar kategori. Mengaktifkan option ini aman dan forward-compatible, tetapi jangan mengandalkannya untuk menampilkan angka langsung pada plot saat ini.
Perhatikan adanya nama yang sama: ChartWidget::labels() wajib diimplementasikan dan mengembalikan label x-axis, sedangkan ChartOptions::labels() adalah boolean option yang dijelaskan di sini.
Contoh lengkap
Berikut adalah examples/app/Panels/Admin/Widgets/UserGrowth.php, sebuah widget yang memiliki filter, menggunakan lazy loading, dan membangun labels() serta series() dari satu query:
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Widgets;
use App\Models\User;
use Illuminate\Support\Facades\Date;
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\FormSchema;
use PandaPanel\Widgets\ChartWidget;
use PandaPanel\Widgets\Enums\ChartVariant;
use PandaPanel\Widgets\Enums\StatColor;
use PandaPanel\Widgets\Support\ChartOptions;
use PandaPanel\Widgets\Support\ChartSeries;
final class UserGrowth extends ChartWidget
{
protected static int $sort = 30;
protected static bool $lazy = true;
protected static ChartVariant $variant = ChartVariant::Area;
protected static ?string $heading = 'Sign-ups';
protected static ?string $description = 'New accounts per month.';
protected static int $maxHeight = 200;
/** @var list<string> */
private array $labels = [];
public function filterSchema(): FormSchema
{
return FormSchema::make()->schema([
Select::make('months')
->label('Window')
->options([
'6' => 'Last 6 months',
'12' => 'Last 12 months',
'24' => 'Last 24 months',
])
->default('6'),
]);
}
public function options(): ChartOptions
{
return ChartOptions::make()->legend(false)->curved()->filled();
}
/**
* @return list<string>
*/
public function labels(): array
{
$this->build();
return $this->labels;
}
/**
* @return list<ChartSeries>
*/
public function series(): array
{
$counts = $this->build();
return [
ChartSeries::make('Sign-ups', array_values($counts))->color(StatColor::Info),
];
}
/**
* @return array<string, int>
*/
private function build(): array
{
$months = max(1, min(24, (int) $this->filter('months', 6)));
$start = Date::now()->startOfMonth()->subMonths($months - 1);
// One grouped query rather than one per month.
$rows = User::query()
->where('created_at', '>=', $start)
->get(['created_at'])
->groupBy(static fn (User $user): string => $user->created_at?->format('Y-m') ?? '')
->map->count();
$labels = [];
$counts = [];
for ($offset = 0; $offset < $months; $offset++) {
$month = $start->copy()->addMonths($offset);
$key = $month->format('Y-m');
$labels[] = $month->format('M');
$counts[$key] = (int) ($rows[$key] ?? 0);
}
$this->labels = $labels;
return $counts;
}
}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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
Perhatikan clamp pada value filter. Select hanya mendeklarasikan tiga option dan schema memang sudah membuang value yang tidak dideklarasikan, tetapi widget tetap membatasi bagaimana value tersebut digunakan. Integer yang berasal dari query string tetap tidak layak dipercaya secara langsung untuk menentukan beban query chart.
Hal yang perlu diperhatikan
- Stub
widget-chartyang ditulis generator mendeklarasikanprotected static string $variant = 'bar';, yang tidak sesuai dengan property parentprotected static ChartVariant $variant. Setelah generate, ubah menjadiprotected static ChartVariant $variant = ChartVariant::Bar;dan import enum-nya, atau publish stub Anda sendiri denganphp artisan vendor:publish --tag=panda-panel-stubs. labels()danseries()dipanggil secara terpisah saat serialisasi. Jika keduanya menjalankan query, lakukan memoization; jika tidak, satu chart dapat menjalankan dua query yang sama.$variant,$maxHeight,$heading, dan$descriptionbersifat static. Chart yang harus mengubah type berdasarkan pengguna sebaiknya dibuat sebagai custom widget.ChartOptions::labels()diserialisasi tetapi belum digambar;ChartOptions::stacked()hanya berpengaruh pada bar.ChartVariant::Doughnutsaat ini dirender melalui jalur bar. Variant tersebut belum menghasilkan ring chart.- Series yang lebih pendek daripada
labels()hanya berhenti pada value terakhir; tidak ada interpolation maupun padding. Bangun labels dan series dari loop yang sama. - Semua kebutuhan yang tidak dapat diekspresikan oleh closed option set sebaiknya dibuat sebagai custom Vue widget. Itu adalah boundary desain yang disengaja, bukan sekadar fitur yang belum ditambahkan.