ES
games/xencenter-xenadmin-xcp-jumphp / src / Gui / LineChart.php · commit fb1b7c9
<?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;    }}