diff options
Diffstat (limited to 'library/Icinga/Chart/Graph')
-rw-r--r-- | library/Icinga/Chart/Graph/BarGraph.php | 163 | ||||
-rw-r--r-- | library/Icinga/Chart/Graph/LineGraph.php | 202 | ||||
-rw-r--r-- | library/Icinga/Chart/Graph/StackedGraph.php | 88 | ||||
-rw-r--r-- | library/Icinga/Chart/Graph/Tooltip.php | 143 |
4 files changed, 596 insertions, 0 deletions
diff --git a/library/Icinga/Chart/Graph/BarGraph.php b/library/Icinga/Chart/Graph/BarGraph.php new file mode 100644 index 0000000..be142bf --- /dev/null +++ b/library/Icinga/Chart/Graph/BarGraph.php @@ -0,0 +1,163 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Chart\Graph; + +use DOMElement; +use Icinga\Chart\Primitive\Animation; +use Icinga\Chart\Primitive\Drawable; +use Icinga\Chart\Primitive\Rect; +use Icinga\Chart\Primitive\Styleable; +use Icinga\Chart\Render\RenderContext; + +/** + * Bar graph implementation + */ +class BarGraph extends Styleable implements Drawable +{ + /** + * The dataset order + * + * @var int + */ + private $order = 0; + + /** + * The width of the bars. + * + * @var int + */ + private $barWidth = 3; + + /** + * The dataset to use for this bar graph + * + * @var array + */ + private $dataSet; + + /** + * The tooltips + * + * @var + */ + private $tooltips; + + /** + * All graphs + * + * @var + */ + private $graphs; + + /** + * Create a new BarGraph with the given dataset + * + * @param array $dataSet An array of data points + * @param int $order The graph number displayed by this BarGraph + * @param array $tooltips The tooltips to display for each value + */ + public function __construct( + array $dataSet, + array &$graphs, + $order, + array $tooltips = null + ) { + $this->order = $order; + $this->dataSet = $dataSet; + + $this->tooltips = $tooltips; + $ts = []; + foreach ($this->tooltips as $value) { + $ts[] = $value; + } + $this->tooltips = $ts; + + $this->graphs = $graphs; + } + + /** + * Apply configuration styles from the $cfg + * + * @param array $cfg The configuration as given in the drawBars call + */ + public function setStyleFromConfig(array $cfg) + { + foreach ($cfg as $elem => $value) { + if ($elem === 'color') { + $this->setFill($value); + } elseif ($elem === 'width') { + $this->setStrokeWidth($value); + } + } + } + + /** + * Draw a single rectangle + * + * @param array $point The + * @param string $fill The fill color to use + * @param $strokeWidth + * @param ?int $index + * + * @return Rect + */ + private function drawSingleBar($point, $fill, $strokeWidth, $index = null) + { + $rect = new Rect($point[0] - ($this->barWidth / 2), $point[1], $this->barWidth, 100 - $point[1]); + $rect->setFill($fill); + $rect->setStrokeWidth($strokeWidth); + $rect->setStrokeColor('black'); + if (isset($index)) { + $rect->setAttribute('data-icinga-graph-index', $index); + } + $rect->setAttribute('data-icinga-graph-type', 'bar'); + $rect->setAdditionalStyle(['clip-path' => 'url(#clip)']); + return $rect; + } + + /** + * Render this BarChart + * + * @param RenderContext $ctx The rendering context to use for drawing + * + * @return DOMElement $dom Element + */ + public function toSvg(RenderContext $ctx) + { + $doc = $ctx->getDocument(); + $group = $doc->createElement('g'); + $idx = 0; + + if (count($this->dataSet) > 15) { + $this->barWidth = 2; + } + if (count($this->dataSet) > 25) { + $this->barWidth = 1; + } + + foreach ($this->dataSet as $x => $point) { + // add white background bar, to prevent other bars from altering transparency effects + $bar = $this->drawSingleBar($point, 'white', $this->strokeWidth, $idx++)->toSvg($ctx); + $group->appendChild($bar); + + // draw actual bar + $bar = $this->drawSingleBar($point, $this->fill, $this->strokeWidth)->toSvg($ctx); + if (isset($this->tooltips[$x])) { + $data = array( + 'label' => isset($this->graphs[$this->order]['label']) ? + strtolower($this->graphs[$this->order]['label']) : '', + 'color' => isset($this->graphs[$this->order]['color']) ? + strtolower($this->graphs[$this->order]['color']) : '#fff' + ); + $format = isset($this->graphs[$this->order]['tooltip']) + ? $this->graphs[$this->order]['tooltip'] : null; + $title = $ctx->getDocument()->createElement('title'); + $title->textContent = $this->tooltips[$x]->renderNoHtml($this->order, $data, $format); + $bar->appendChild($title); + } + $group->appendChild($bar); + } + return $group; + } +} diff --git a/library/Icinga/Chart/Graph/LineGraph.php b/library/Icinga/Chart/Graph/LineGraph.php new file mode 100644 index 0000000..21f930a --- /dev/null +++ b/library/Icinga/Chart/Graph/LineGraph.php @@ -0,0 +1,202 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + + +namespace Icinga\Chart\Graph; + +use DOMElement; +use Icinga\Chart\Primitive\Drawable; +use Icinga\Chart\Primitive\Path; +use Icinga\Chart\Primitive\Circle; +use Icinga\Chart\Primitive\Styleable; +use Icinga\Chart\Render\RenderContext; + +/** + * LineGraph implementation for drawing a set of datapoints as + * a connected path + */ +class LineGraph extends Styleable implements Drawable +{ + /** + * The dataset to use + * + * @var array + */ + private $dataset; + + /** + * True to show dots for each datapoint + * + * @var bool + */ + private $showDataPoints = false; + + /** + * When true, the path will be discrete, i.e. showing hard steps instead of a direct line + * + * @var bool + */ + private $isDiscrete = false; + + /** + * The tooltips + * + * @var + */ + private $tooltips; + + /** @var array */ + private $graphs; + + /** @var int */ + private $order; + + /** + * The default stroke width + * @var int + */ + public $strokeWidth = 5; + + /** + * The size of the displayed dots + * + * @var int + */ + public $dotWith = 0; + + /** + * Create a new LineGraph displaying the given dataset + * + * @param array $dataset An array of [x, y] arrays to display + */ + public function __construct( + array $dataset, + array &$graphs, + $order, + array $tooltips = null + ) { + usort($dataset, array($this, 'sortByX')); + $this->dataset = $dataset; + $this->graphs = $graphs; + + $this->tooltips = $tooltips; + $ts = []; + foreach ($this->tooltips as $value) { + $ts[] = $value; + } + $this->tooltips = $ts; + $this->order = $order; + } + + /** + * Set datapoints to be emphased via dots + * + * @param bool $bool True to enable datapoints, otherwise false + */ + public function setShowDataPoints($bool) + { + $this->showDataPoints = $bool; + } + + /** + * Sort the daset by the xaxis + * + * @param array $v1 + * @param array $v2 + * @return int + */ + private function sortByX(array $v1, array $v2) + { + if ($v1[0] === $v2[0]) { + return 0; + } + return ($v1[0] < $v2[0]) ? -1 : 1; + } + + /** + * Configure this style + * + * @param array $cfg The configuration as given in the drawLine call + */ + public function setStyleFromConfig(array $cfg) + { + $fill = false; + foreach ($cfg as $elem => $value) { + if ($elem === 'color') { + $this->setStrokeColor($value); + } elseif ($elem === 'width') { + $this->setStrokeWidth($value); + } elseif ($elem === 'showPoints') { + $this->setShowDataPoints($value); + } elseif ($elem === 'fill') { + $fill = $value; + } elseif ($elem === 'discrete') { + $this->isDiscrete = true; + } + } + if ($fill) { + $this->setFill($this->strokeColor); + $this->setStrokeColor('black'); + } + } + + /** + * Render this BarChart + * + * @param RenderContext $ctx The rendering context to use for drawing + * + * @return DOMElement $dom Element + */ + public function toSvg(RenderContext $ctx) + { + $path = new Path($this->dataset); + if ($this->isDiscrete) { + $path->setDiscrete(true); + } + $path->setStrokeColor($this->strokeColor); + $path->setStrokeWidth($this->strokeWidth); + + $path->setAttribute('data-icinga-graph-type', 'line'); + if ($this->fill !== 'none') { + $firstX = $this->dataset[0][0]; + $lastX = $this->dataset[count($this->dataset)-1][0]; + $path->prepend(array($firstX, 100)) + ->append(array($lastX, 100)); + $path->setFill($this->fill); + } + + $path->setAdditionalStyle(['clip-path' => 'url(#clip)']); + $path->setId($this->id ?? uniqid('line-graph-')); + $group = $path->toSvg($ctx); + + foreach ($this->dataset as $x => $point) { + if ($this->showDataPoints === true) { + $dot = new Circle($point[0], $point[1], $this->dotWith); + $dot->setFill($this->strokeColor); + $group->appendChild($dot->toSvg($ctx)); + } + + // Draw invisible circle for tooltip hovering + if (isset($this->tooltips[$x])) { + $invisible = new Circle($point[0], $point[1], 20); + $invisible->setFill($this->strokeColor); + $invisible->setAdditionalStyle(['opacity' => '0.0']); + $data = array( + 'label' => isset($this->graphs[$this->order]['label']) ? + strtolower($this->graphs[$this->order]['label']) : '', + 'color' => isset($this->graphs[$this->order]['color']) ? + strtolower($this->graphs[$this->order]['color']) : '#fff' + ); + $format = isset($this->graphs[$this->order]['tooltip']) + ? $this->graphs[$this->order]['tooltip'] : null; + $title = $ctx->getDocument()->createElement('title'); + $title->textContent = $this->tooltips[$x]->renderNoHtml($this->order, $data, $format); + $invisibleRendered = $invisible->toSvg($ctx); + $invisibleRendered->appendChild($title); + $group->appendChild($invisibleRendered); + } + } + + return $group; + } +} diff --git a/library/Icinga/Chart/Graph/StackedGraph.php b/library/Icinga/Chart/Graph/StackedGraph.php new file mode 100644 index 0000000..49801a9 --- /dev/null +++ b/library/Icinga/Chart/Graph/StackedGraph.php @@ -0,0 +1,88 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Chart\Graph; + +use DOMElement; +use Icinga\Chart\Primitive\Drawable; +use Icinga\Chart\Render\RenderContext; + +/** + * Graph implementation that stacks several graphs and displays them in a cumulative way + */ +class StackedGraph implements Drawable +{ + /** + * All graphs displayed in this stackedgraph + * + * @var array + */ + private $stack = array(); + + /** + * An associative array containing x points as the key and an array of y values as the value + * + * @var array + */ + private $points = array(); + + /** + * Add a graph to this stack and aggregate the values on the fly + * + * This modifies the dataset as a side effect + * + * @param array $subGraph + */ + public function addGraph(array &$subGraph) + { + foreach ($subGraph['data'] as &$point) { + $x = $point[0]; + if (!isset($this->points[$x])) { + $this->points[$x] = 0; + } + // store old y-value for displaying the actual (non-aggregated) + // value in the tooltip + $point[2] = $point[1]; + + $this->points[$x] += $point[1]; + $point[1] = $this->points[$x]; + } + } + + /** + * Add a graph to the stack + * + * @param $graph + */ + public function addToStack($graph) + { + $this->stack[] = $graph; + } + + /** + * Empty the stack + * + * @return bool + */ + public function stackEmpty() + { + return empty($this->stack); + } + + /** + * Render this stack in the correct order + * + * @param RenderContext $ctx The context to use for rendering + * + * @return DOMElement The SVG representation of this graph + */ + public function toSvg(RenderContext $ctx) + { + $group = $ctx->getDocument()->createElement('g'); + $renderOrder = array_reverse($this->stack); + foreach ($renderOrder as $stackElem) { + $group->appendChild($stackElem->toSvg($ctx)); + } + return $group; + } +} diff --git a/library/Icinga/Chart/Graph/Tooltip.php b/library/Icinga/Chart/Graph/Tooltip.php new file mode 100644 index 0000000..7236685 --- /dev/null +++ b/library/Icinga/Chart/Graph/Tooltip.php @@ -0,0 +1,143 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Chart\Graph; + +/** + * A tooltip that stores and aggregates information about displayed data + * points of a graph and replaces them in a format string to render the description + * for specific data points of the graph. + * + * When render() is called, placeholders for the keys for each data entry will be replaced by + * the current value of this data set and the formatted string will be returned. + * The content of the replaced keys can change for each data set and depends on how the data + * is passed to this class. There are several types of properties: + * + * <ul> + * <li>Global properties</li>: Key-value pairs that stay the same every time render is called, and are + * passed to an instance in the constructor. + * <li>Aggregated properties</li>: Global properties that are created automatically from + * all attached data points. + * <li>Local properties</li>: Key-value pairs that only apply to a single data point and + * are passed to the render-function. + * </ul> + */ +class Tooltip +{ + /** + * The default format string used + * when no other format is specified + * + * @var string + */ + private $defaultFormat; + + /** + * All aggregated points + * + * @var array + */ + private $points = array(); + + /** + * Contains all static replacements + * + * @var array + */ + private $data = array( + 'sum' => 0 + ); + + /** + * Used to format the displayed tooltip. + * + * @var string + */ + protected $tooltipFormat; + + /** + * Create a new tooltip with the specified default format string + * + * Allows you to set the global data for this tooltip, that is displayed every + * time render is called. + * + * @param array $data Map of global properties + * @param string $format The default format string + */ + public function __construct( + $data = array(), + $format = '<b>{title}</b>: {value} {label}' + ) { + $this->data = array_merge($this->data, $data); + $this->defaultFormat = $format; + } + + /** + * Add a single data point to update the aggregated properties for this tooltip + * + * @param $point array Contains the (x,y) values of the data set + */ + public function addDataPoint($point) + { + // set x-value + if (!isset($this->data['title'])) { + $this->data['title'] = $point[0]; + } + + // aggregate y-values + $y = (int)$point[1]; + if (isset($point[2])) { + // load original value in case value already aggregated + $y = (int)$point[2]; + } + + if (!isset($this->data['min']) || $this->data['min'] > $y) { + $this->data['min'] = $y; + } + if (!isset($this->data['max']) || $this->data['max'] < $y) { + $this->data['max'] = $y; + } + $this->data['sum'] += $y; + $this->points[] = $y; + } + + /** + * Format the tooltip for a certain data point + * + * @param array $order Which data set to render + * @param array $data The local data for this tooltip + * @param string $format Use a custom format string for this data set + * + * @return mixed|string The tooltip value + */ + public function render($order, $data = array(), $format = null) + { + if (isset($format)) { + $str = $format; + } else { + $str = $this->defaultFormat; + } + $data['value'] = $this->points[$order]; + foreach (array_merge($this->data, $data) as $key => $value) { + $str = str_replace('{' . $key . '}', $value, $str); + } + return $str; + } + + /** + * Format the tooltip for a certain data point but remove all + * occurring html tags + * + * This is useful for rendering clean tooltips on client without JavaScript + * + * @param array $order Which data set to render + * @param array $data The local data for this tooltip + * @param string $format Use a custom format string for this data set + * + * @return mixed|string The tooltip value, without any HTML tags + */ + public function renderNoHtml($order, $data, $format) + { + return strip_tags($this->render($order, $data, $format)); + } +} |