diff options
Diffstat (limited to 'library/Icinga/Chart/Donut.php')
-rw-r--r-- | library/Icinga/Chart/Donut.php | 465 |
1 files changed, 465 insertions, 0 deletions
diff --git a/library/Icinga/Chart/Donut.php b/library/Icinga/Chart/Donut.php new file mode 100644 index 0000000..9d2a2a8 --- /dev/null +++ b/library/Icinga/Chart/Donut.php @@ -0,0 +1,465 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Chart; + +use Icinga\Web\Url; + +/** Donut chart implementation */ +class Donut +{ + /** + * Big label in the middle of the donut, color is critical (red) + * + * @var string + */ + protected $labelBig; + + /** + * Url behind the big label + * + * @var Url + */ + protected $labelBigUrl; + + /** + * The state the big label shall indicate + * + * @var string|null + */ + protected $labelBigState = 'critical'; + + /** + * Small label in the lower part of the donuts hole + * + * @var string + */ + protected $labelSmall; + + /** + * Thickness of the donut ring + * + * @var int + */ + protected $thickness = 6; + + /** + * Radius based of 100 to simplify the calculations + * + * 100 / (2 * M_PI) + * + * @var float + */ + protected $radius = 15.9154943092; + + /** + * Color of the hole in the donut + * + * Transparent by default so it can be placed anywhere with ease + * + * @var string + */ + protected $centerColor = 'transparent'; + + /** + * The different colored parts that represent the data + * + * @var array + */ + protected $slices = array(); + + /** + * The total amount of data units + * + * @var int + */ + protected $count = 0; + + /** + * Adds a colored part that represent the data + * + * @param integer $data Units of data + * @param array $attributes HTML attributes for this slice. (For example ['class' => 'slice-state-ok']) + * + * @return $this + */ + public function addSlice($data, $attributes = array()) + { + $this->slices[] = array($data, $attributes); + + $this->count += $data; + + return $this; + } + + /** + * Set the thickness for this Donut + * + * @param integer $thickness + * + * @return $this + */ + public function setThickness($thickness) + { + $this->thickness = $thickness; + + return $this; + } + /** + * Get the thickness for this Donut + * + * @return integer + */ + public function getThickness() + { + return $this->thickness; + } + + /** + * Set the center color for this Donut + * + * @param string $centerColor + * + * @return $this + */ + public function setCenterColor($centerColor) + { + $this->centerColor = $centerColor; + + return $this; + } + + /** + * Get the center color for this Donut + * + * @return string + */ + public function getCenterColor() + { + return $this->centerColor; + } + + /** + * Set the text of the big label + * + * @param string $labelBig + * + * @return $this + */ + public function setLabelBig($labelBig) + { + $this->labelBig = $labelBig; + + return $this; + } + + /** + * Get the text of the big label + * + * @return string + */ + public function getLabelBig() + { + return $this->labelBig; + } + + /** + * Set the url behind the big label + * + * @param Url $labelBigUrl + * + * @return $this + */ + public function setLabelBigUrl($labelBigUrl) + { + $this->labelBigUrl = $labelBigUrl; + + return $this; + } + + /** + * Get the url behind the big label + * + * @return Url + */ + public function getLabelBigUrl() + { + return $this->labelBigUrl; + } + + /** + * Get whether the big label shall be eye-catching + * + * @return bool + */ + public function getLabelBigEyeCatching() + { + return $this->labelBigState !== null; + } + + /** + * Set whether the big label shall be eye-catching + * + * @param bool $labelBigEyeCatching + * + * @return $this + */ + public function setLabelBigEyeCatching($labelBigEyeCatching = true) + { + $this->labelBigState = $labelBigEyeCatching ? 'critical' : null; + + return $this; + } + + /** + * Get the state the big label shall indicate + * + * @return string|null + */ + public function getLabelBigState() + { + return $this->labelBigState; + } + + /** + * Set the state the big label shall indicate + * + * @param string|null $labelBigState + * + * @return $this + */ + public function setLabelBigState($labelBigState) + { + $this->labelBigState = $labelBigState; + + return $this; + } + + /** + * Set the text of the small label + * + * @param string $labelSmall + * + * @return $this + */ + public function setLabelSmall($labelSmall) + { + $this->labelSmall = $labelSmall; + + return $this; + } + + /** + * Get the text of the small label + * + * @return string + */ + public function getLabelSmall() + { + return $this->labelSmall; + } + + /** + * Put together all slices of this Donut + * + * @return array $svg + */ + protected function assemble() + { + // svg tag containing the ring + $svg = array( + 'tag' => 'svg', + 'attributes' => array( + 'xmlns' => 'http://www.w3.org/2000/svg', + 'viewbox' => '0 0 40 40', + 'class' => 'donut-graph' + ), + 'content' => array() + ); + + // Donut hole + $svg['content'][] = array( + 'tag' => 'circle', + 'attributes' => array( + 'cx' => 20, + 'cy' => 20, + 'r' => sprintf('%F', $this->radius), + 'fill' => $this->getCenterColor() + ) + ); + + // When there is no data show gray circle + $svg['content'][] = array( + 'tag' => 'circle', + 'attributes' => array( + 'aria-hidden' => true, + 'cx' => 20, + 'cy' => 20, + 'r' => sprintf('%F', $this->radius), + 'fill' => $this->getCenterColor(), + 'stroke-width' => $this->getThickness(), + 'class' => 'slice-state-not-checked' + ) + ); + + $slices = $this->slices; + + if ($this->count !== 0) { + array_walk($slices, function (&$slice) { + $slice[0] = round(100 / $this->count * $slice[0], 2); + }); + } + + // on 0 the donut would start at "3 o'clock" and the offset shifts counterclockwise + $offset = 25; + + foreach ($slices as $slice) { + $svg['content'][] = array( + 'tag' => 'circle', + 'attributes' => $slice[1] + array( + 'cx' => 20, + 'cy' => 20, + 'r' => sprintf('%F', $this->radius), + 'fill' => 'transparent', + 'stroke-width' => $this->getThickness(), + 'stroke-dasharray' => sprintf('%F', $slice[0]) + . ' ' + . sprintf('%F', (99.9 - $slice[0])), // 99.9 prevents gaps (slight overlap) + 'stroke-dashoffset' => sprintf('%F', $offset) + ) + ); + // negative values shift in the clockwise direction + $offset -= $slice[0]; + } + + $result = array( + 'tag' => 'div', + 'content' => array($svg) + ); + + $labelBig = (string) $this->getLabelBig(); + $labelSmall = (string) $this->getLabelSmall(); + + if ($labelBig !== '' || $labelSmall !== '') { + $labels = array( + 'tag' => 'div', + 'attributes' => array( + 'class' => 'donut-label' + ), + 'content' => array() + ); + + if ($labelBig !== '') { + $labels['content'][] = + array( + 'tag' => 'a', + 'attributes' => array( + 'aria-label' => $labelBig . ' ' . $labelSmall, + 'href' => $this->getLabelBigUrl() ? $this->getLabelBigUrl()->getAbsoluteUrl() : null, + 'class' => $this->labelBigState === null + ? 'donut-label-big' + : 'donut-label-big state-' . $this->labelBigState + ), + 'content' => $this->shortenLabel($labelBig) + ); + } + + if ($labelSmall !== '') { + $labels['content'][] = array( + 'tag' => 'p', + 'attributes' => array( + 'class' => 'donut-label-small', + 'x' => '50%', + 'y' => '50%' + ), + 'content' => $labelSmall + ); + } + + $result['content'][] = $labels; + } + + return $result; + } + + /** + * Shorten the label to 3 digits if it is numeric + * + * 10 => 10 ... 1111 => ~1k ... 1888 => ~2k + * + * @param int|string $label + * + * @return string + */ + protected function shortenLabel($label) + { + if (is_numeric($label) && strlen($label) > 3) { + return round($label, -3)/1000 . 'k'; + } + + return $label; + } + + protected function encode($content) + { + return htmlspecialchars($content, ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8', true); + } + + protected function renderAttributes(array $attributes) + { + $html = array(); + + foreach ($attributes as $name => $value) { + if ($value === null) { + continue; + } + + if (is_bool($value) && $value) { + $html[] = $name; + continue; + } + + if (is_array($value)) { + $value = implode(' ', $value); + } + + $html[] = "$name=\"" . $this->encode($value) . '"'; + } + + return implode(' ', $html); + } + + protected function renderContent(array $element) + { + $tag = $element['tag']; + $attributes = isset($element['attributes']) ? $element['attributes'] : array(); + $content = isset($element['content']) ? $element['content'] : null; + + $html = array( + // rtrim because attributes may be empty + rtrim("<$tag " . $this->renderAttributes($attributes)) + . ">" + ); + + if ($content !== null) { + if (is_array($content)) { + foreach ($content as $child) { + $html[] = is_array($child) ? $this->renderContent($child) : $this->encode($child); + } + } else { + $html[] = $this->encode($content); + } + } + + $html[] = "</$tag>"; + + return implode("\n", $html); + } + + public function render() + { + $svg = $this->assemble(); + + return $this->renderContent($svg); + } +} |