<?phpnamespace App\Gui;use JumPHP\Gui\Theme;use JumPHP\Renderer\Renderer;/** * One graph of the Performance tab: the last minutes of a few series (CPU, memory, network, disk), a grid, the scale, a legend. * series: [['label' => 'CPU 0', 'color' => '#...', 'points' => [[t, v], ...]]]; format: percent, bytes, rate (bytes per second). * * @property string $title * @property string $format * @property float|null $max */final class LineChart extends Canvas{ public const DEFAULTS = ['height' => 210, 'width' => 'fill', 'background' => '#ffffff', 'border' => '#e1e5eb', 'title' => '', 'format' => 'percent', 'max' => null]; public const COLORS = ['#3a7bd5', '#e8912d', '#2ea043', '#d1242f', '#8250df', '#1f9aa5', '#bf8700', '#cf4f9a']; /** @var list<array{label: string, color?: string, points: list<array{0: int, 1: float}>}> */ public array $series = []; public function setSeries(array $series): void { foreach ($series as $i => &$s) { $s['color'] ??= self::COLORS[$i % count(self::COLORS)]; } $this->series = $series; } public static function format(float $v, string $format): string { return match ($format) { 'percent' => round($v * 100) . '%', 'bytes' => Units::bytes($v), 'rate' => Units::bytes($v) . '/s', default => (string)round($v, 2), }; } protected function paint(Renderer $r, Theme $t): void { $font = $t->font($t->fontSize * 0.85); $titleFont = $t->font(null, true); $left = $this->x + $t->px(64); $top = $this->y + $t->px(30); $right = $this->x + $this->w - $t->px(12); $bottom = $this->y + $this->h - $t->px(40); $titleFont->draw($r, \JumPHP\Util\Lang::t((string)$this->props['title']), $this->x + $t->px(10), $this->y + $t->px(7), 1, '#1d3c6e'); $format = (string)$this->props['format']; $tMin = PHP_INT_MAX; $tMax = 0; $vMax = 0.0; foreach ($this->series as $s) { foreach ($s['points'] as [$tt, $v]) { $tMin = min($tMin, $tt); $tMax = max($tMax, $tt); $vMax = max($vMax, $v); } } $max = $this->props['max'] !== null ? (float)$this->props['max'] : self::niceMax($vMax, $format); $r->fillRect($left, $top, $right - $left, $bottom - $top, '#fbfcfd'); for ($k = 0; $k <= 4; $k++) { $gy = $bottom - ($bottom - $top) * $k / 4; $r->fillRect($left, $gy, $right - $left, 1, $k === 0 ? '#9a9a9a' : '#e4e4e4'); $font->draw($r, self::format($max * $k / 4, $format), $left - $t->px(6), $gy - $font->height() / 2, 1, '#666666', 'right'); } $r->fillRect($left, $top, 1, $bottom - $top, '#9a9a9a'); if ($tMax <= $tMin) { $font->draw($r, \JumPHP\Util\Lang::t('Waiting for data…'), ($left + $right) / 2, ($top + $bottom) / 2 - $font->height() / 2, 1, '#8a8a8a', 'center'); } else { $span = $tMax - $tMin; for ($k = 0; $k <= 4; $k++) { $tt = (int)($tMin + $span * $k / 4); $gx = $left + ($right - $left) * $k / 4; if ($k > 0) { $r->fillRect($gx, $top, 1, $bottom - $top, '#efefef'); } $font->draw($r, date('H:i:s', $tt), $gx, $bottom + $t->px(3), 1, '#666666', $k === 0 ? 'left' : ($k === 4 ? 'right' : 'center')); } foreach ($this->series as $s) { $prev = null; foreach ($s['points'] as [$tt, $v]) { $px = $left + ($right - $left) * ($tt - $tMin) / $span; $py = $bottom - ($bottom - $top) * min(1.0, $v / max(1e-9, $max)); if ($prev !== null) { $r->drawLine($prev[0], $prev[1], $px, $py, $s['color']); $r->drawLine($prev[0], $prev[1] - 1, $px, $py - 1, $s['color']); } $prev = [$px, $py]; } } } // the legend, with the last value $lx = $left; $ly = $this->y + $this->h - $t->px(18); foreach ($this->series as $s) { $points = $s['points']; $last = $points === [] ? '' : ' ' . self::format((float)end($points)[1], $format); $text = \JumPHP\Util\Lang::t((string)$s['label']) . $last; $w = $font->width($text) + $t->px(26); if ($lx + $w > $right) { break; } $r->fillRect($lx, $ly + $font->height() / 2 - $t->px(2), $t->px(12), $t->px(4), $s['color']); $font->draw($r, $text, $lx + $t->px(16), $ly, 1, '#333333'); $lx += $w; } } public static function niceMax(float $v, string $format): float { if ($format === 'percent') { return $v <= 0.25 ? 0.25 : ($v <= 0.5 ? 0.5 : 1.0); } if ($v <= 0) { return 1; } $base = $format === 'percent' ? 10 : 1024; $p = 1.0; while ($p * $base <= $v) { $p *= $base; } foreach ([1, 2, 4, 5, 8, 10, 16, 20, 32, 50, 64, 100, 128, 200, 256, 500, 512, 1024] as $m) { if ($p * $m >= $v * 1.1) { return $p * $m; } } return $p * $base; }}