|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace App\Filament\Widgets; |
| 4 | + |
| 5 | +use App\Models\Setting; |
| 6 | +use Filament\Widgets\ChartWidget; |
| 7 | +use SilkPanel\SilkroadModels\Models\Shard\AbstractChar; |
| 8 | + |
| 9 | +class PlayerLevelDistributionWidget extends ChartWidget |
| 10 | +{ |
| 11 | + protected static ?int $sort = 10; |
| 12 | + |
| 13 | + protected ?string $maxHeight = '280px'; |
| 14 | + |
| 15 | + protected int | string | array $columnSpan = [ |
| 16 | + 'default' => 'full', |
| 17 | + 'lg' => 3, |
| 18 | + 'xl' => 3, |
| 19 | + ]; |
| 20 | + |
| 21 | + public function getHeading(): string |
| 22 | + { |
| 23 | + return __('filament/characters.widget_level_distribution.heading'); |
| 24 | + } |
| 25 | + |
| 26 | + public function getDescription(): ?string |
| 27 | + { |
| 28 | + return __('filament/characters.widget_level_distribution.description'); |
| 29 | + } |
| 30 | + |
| 31 | + protected function getData(): array |
| 32 | + { |
| 33 | + /** @var AbstractChar $charModel */ |
| 34 | + $charModel = app(AbstractChar::class); |
| 35 | + |
| 36 | + $step = 10; |
| 37 | + |
| 38 | + // 1. Prefer the configured level cap (sro_cap setting) |
| 39 | + // 2. Fall back to the actual max level in the DB |
| 40 | + $cap = (int) Setting::get('sro_cap', 0); |
| 41 | + $max = $cap > 0 |
| 42 | + ? $cap |
| 43 | + : (int) $charModel->newQuery() |
| 44 | + ->where('CharID', '!=', 0) |
| 45 | + ->where('CharName16', '!=', 'dummy') |
| 46 | + ->max('CurLevel'); |
| 47 | + |
| 48 | + // Round up to the next full step so the last bucket is complete |
| 49 | + $max = (int) (ceil($max / $step) * $step); |
| 50 | + |
| 51 | + // Build buckets: 1–10, 11–20, ... |
| 52 | + $buckets = []; |
| 53 | + for ($from = 1; $from <= $max; $from += $step) { |
| 54 | + $to = $from + $step - 1; |
| 55 | + $buckets[] = ['from' => $from, 'to' => $to, 'label' => "{$from}–{$to}"]; |
| 56 | + } |
| 57 | + |
| 58 | + $counts = $charModel->newQuery() |
| 59 | + ->where('CharID', '!=', 0) |
| 60 | + ->where('CharName16', '!=', 'dummy') |
| 61 | + ->where('CurLevel', '>=', 1) |
| 62 | + ->selectRaw('CurLevel, COUNT(*) as cnt') |
| 63 | + ->groupBy('CurLevel') |
| 64 | + ->pluck('cnt', 'CurLevel'); |
| 65 | + |
| 66 | + $data = []; |
| 67 | + $labels = []; |
| 68 | + $colors = []; |
| 69 | + |
| 70 | + foreach ($buckets as $bucket) { |
| 71 | + $total = 0; |
| 72 | + for ($lvl = $bucket['from']; $lvl <= $bucket['to']; $lvl++) { |
| 73 | + $total += $counts->get($lvl, 0); |
| 74 | + } |
| 75 | + |
| 76 | + // Skip completely empty high-level buckets at the tail |
| 77 | + $data[] = $total; |
| 78 | + $labels[] = $bucket['label']; |
| 79 | + |
| 80 | + // Color gradient: low levels = blue, mid = purple, high = amber |
| 81 | + $ratio = ($bucket['from'] - 1) / ($max - 1); |
| 82 | + if ($ratio < 0.5) { |
| 83 | + $r = (int) (99 + (124 - 99) * ($ratio * 2)); |
| 84 | + $g = (int) (102 + (58 - 102) * ($ratio * 2)); |
| 85 | + $b = (int) (241 + (237 - 241) * ($ratio * 2)); |
| 86 | + } else { |
| 87 | + $r = (int) (124 + (245 - 124) * (($ratio - 0.5) * 2)); |
| 88 | + $g = (int) (58 + (158 - 58) * (($ratio - 0.5) * 2)); |
| 89 | + $b = (int) (237 + (11 - 237) * (($ratio - 0.5) * 2)); |
| 90 | + } |
| 91 | + $colors[] = "rgba({$r},{$g},{$b},0.85)"; |
| 92 | + } |
| 93 | + |
| 94 | + return [ |
| 95 | + 'datasets' => [ |
| 96 | + [ |
| 97 | + 'label' => __('filament/characters.widget_level_distribution.dataset_label'), |
| 98 | + 'data' => $data, |
| 99 | + 'backgroundColor' => $colors, |
| 100 | + 'borderColor' => array_map( |
| 101 | + fn($c) => str_replace('0.85', '1', $c), |
| 102 | + $colors |
| 103 | + ), |
| 104 | + 'borderWidth' => 1, |
| 105 | + 'borderRadius' => 6, |
| 106 | + ], |
| 107 | + ], |
| 108 | + 'labels' => $labels, |
| 109 | + ]; |
| 110 | + } |
| 111 | + |
| 112 | + protected function getType(): string |
| 113 | + { |
| 114 | + return 'bar'; |
| 115 | + } |
| 116 | + |
| 117 | + protected function getOptions(): array |
| 118 | + { |
| 119 | + return [ |
| 120 | + 'plugins' => [ |
| 121 | + 'legend' => ['display' => false], |
| 122 | + 'tooltip' => [ |
| 123 | + 'displayColors' => false, |
| 124 | + 'titlePrefix' => 'Level ', |
| 125 | + ], |
| 126 | + ], |
| 127 | + 'scales' => [ |
| 128 | + 'x' => [ |
| 129 | + 'title' => [ |
| 130 | + 'display' => true, |
| 131 | + 'text' => __('filament/characters.widget_level_distribution.x_label'), |
| 132 | + 'color' => 'rgb(107,114,128)', |
| 133 | + 'font' => ['size' => 11], |
| 134 | + ], |
| 135 | + 'grid' => ['display' => false], |
| 136 | + ], |
| 137 | + 'y' => [ |
| 138 | + 'title' => [ |
| 139 | + 'display' => true, |
| 140 | + 'text' => __('filament/characters.widget_level_distribution.y_label'), |
| 141 | + 'color' => 'rgb(107,114,128)', |
| 142 | + 'font' => ['size' => 11], |
| 143 | + ], |
| 144 | + 'beginAtZero' => true, |
| 145 | + 'ticks' => ['precision' => 0], |
| 146 | + ], |
| 147 | + ], |
| 148 | + ]; |
| 149 | + } |
| 150 | +} |
0 commit comments