366 lines
11 KiB
PHP
366 lines
11 KiB
PHP
<?php
|
|
// {{{ICINGA_LICENSE_HEADER}}}
|
|
/**
|
|
* This file is part of Icinga Web 2.
|
|
*
|
|
* Icinga Web 2 - Head for multiple monitoring backends.
|
|
* Copyright (C) 2013 Icinga Development Team
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
* @copyright 2013 Icinga Development Team <info@icinga.org>
|
|
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
|
|
* @author Icinga Development Team <info@icinga.org>
|
|
*
|
|
*/
|
|
// {{{ICINGA_LICENSE_HEADER}}}
|
|
|
|
namespace Icinga\Chart;
|
|
|
|
use \DOMElement;
|
|
use \Icinga\Chart\Chart;
|
|
use \Icinga\Chart\Primitive\Canvas;
|
|
use \Icinga\Chart\Primitive\PieSlice;
|
|
use \Icinga\Chart\Primitive\RawElement;
|
|
use \Icinga\Chart\Primitive\Rect;
|
|
use \Icinga\Chart\Render\RenderContext;
|
|
use \Icinga\Chart\Render\LayoutBox;
|
|
|
|
/**
|
|
* Graphing component for rendering Pie Charts.
|
|
*
|
|
* See the graphs.md documentation for futher information about how to use this component
|
|
*/
|
|
class PieChart extends Chart
|
|
{
|
|
/**
|
|
* Stack multiple pies
|
|
*/
|
|
const STACKED = "stacked";
|
|
|
|
/**
|
|
* Draw multiple pies beneath each other
|
|
*/
|
|
const ROW = "row";
|
|
|
|
/**
|
|
* The drawing stack containing all pie definitions in the order they will be drawn
|
|
*
|
|
* @var array
|
|
*/
|
|
private $pies = array();
|
|
|
|
/**
|
|
* The composition type currently used
|
|
*
|
|
* @var string
|
|
*/
|
|
private $type = PieChart::STACKED;
|
|
|
|
/**
|
|
* Disable drawing of captions when set true
|
|
*
|
|
* @var bool
|
|
*/
|
|
private $noCaption = false;
|
|
|
|
/**
|
|
* Scaling level of the rendered svgs width in percent.
|
|
*
|
|
* @var float
|
|
*/
|
|
private $width = 100;
|
|
|
|
/**
|
|
* Scaling level of the rendered svgs height in percent.
|
|
*
|
|
* @var int
|
|
*/
|
|
private $height = 100;
|
|
|
|
/**
|
|
* Set the size of the rendered pie-chart svg.
|
|
*
|
|
* @param $width int The width in percent.
|
|
*
|
|
* @return self Fluent interface
|
|
*/
|
|
public function setWidth($width)
|
|
{
|
|
$this->width = $width;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the size of the rendered pie-chart svg.
|
|
*
|
|
* @param $height int The height in percent.
|
|
*
|
|
* @return self Fluent interface
|
|
*/
|
|
public function setHeight($height)
|
|
{
|
|
$this->height = $height;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Test if the given pies have the correct format
|
|
*
|
|
* @return bool True when the given pies are correct, otherwise false
|
|
*/
|
|
public function isValidDataFormat()
|
|
{
|
|
foreach ($this->pies as $pie) {
|
|
if (!isset($pie['data']) || !is_array($pie['data'])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Create renderer and normalize the dataset to represent percentage information
|
|
*/
|
|
protected function build()
|
|
{
|
|
$this->renderer = new SVGRenderer(
|
|
$this->type === self::STACKED ? $this->width : count($this->pies) * $this->width,
|
|
$this->width
|
|
);
|
|
foreach ($this->pies as &$pie) {
|
|
$this->normalizeDataSet($pie);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Normalize the given dataset to represent percentage information instead of absolute valuess
|
|
*
|
|
* @param array $pie The pie definition given in the drawPie call
|
|
*/
|
|
private function normalizeDataSet(&$pie)
|
|
{
|
|
$total = array_sum($pie['data']);
|
|
if ($total === 100) {
|
|
return;
|
|
}
|
|
if ($total == 0) {
|
|
return;
|
|
}
|
|
foreach ($pie['data'] as &$slice) {
|
|
$slice = $slice/$total * 100;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draw an arbitrary number of pies in this chart
|
|
*
|
|
* @param array $dataSet,... The pie definition, see graphs.md for further details concerning the format
|
|
*
|
|
* @return self Fluent interface
|
|
*/
|
|
public function drawPie(array $dataSet)
|
|
{
|
|
$dataSets = func_get_args();
|
|
$this->pies += $dataSets;
|
|
foreach ($dataSets as $dataSet) {
|
|
$this->legend->addDataset($dataSet);
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Return the SVG representation of this graph
|
|
*
|
|
* @param RenderContext $ctx The context to use for drawings
|
|
*
|
|
* @return DOMElement The SVG representation of this graph
|
|
*/
|
|
public function toSvg(RenderContext $ctx)
|
|
{
|
|
$outerBox = new Canvas('outerGraph', new LayoutBox(0, 0, 100, 100));
|
|
$innerBox = new Canvas('graph', new LayoutBox(0, 0, 100, 100));
|
|
$labelBox = $ctx->getDocument()->createElement('g');
|
|
$innerBox->getLayout()->setPadding(10, 10, 10, 10);
|
|
$this->createContentClipBox($innerBox);
|
|
$this->renderPies($innerBox, $labelBox);
|
|
$innerBox->addElement(new RawElement($labelBox));
|
|
$outerBox->addElement($innerBox);
|
|
|
|
return $outerBox->toSvg($ctx);
|
|
}
|
|
|
|
/**
|
|
* Render the pies in the draw stack using the selected algorithm for composition
|
|
*
|
|
* @param Canvas $innerBox The canvas to use for inserting the pies
|
|
* @param DOMElement $labelBox The DOM element to add the labels to (so they can't be overlapped by pie elements)
|
|
*/
|
|
private function renderPies(Canvas $innerBox, DOMElement $labelBox)
|
|
{
|
|
if ($this->type === self::STACKED) {
|
|
$this->renderStackedPie($innerBox, $labelBox);
|
|
} else {
|
|
$this->renderPieRow($innerBox, $labelBox);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the color to be used for the given pie slice
|
|
*
|
|
* @param array $pie The pie configuration as provided in the drawPie call
|
|
* @param int $dataIdx The index of the pie slice in the pie configuration
|
|
*
|
|
* @return string The hex color string to use for the pie slice
|
|
*/
|
|
private function getColorForPieSlice(array $pie, $dataIdx)
|
|
{
|
|
if (isset($pie['colors']) && is_array($pie['colors']) && isset($pie['colors'][$dataIdx])) {
|
|
return $pie['colors'][$dataIdx];
|
|
}
|
|
$type = Palette::NEUTRAL;
|
|
if (isset($pie['palette']) && is_array($pie['palette']) && isset($pie['palette'][$dataIdx])) {
|
|
$type = $pie['palette'][$dataIdx];
|
|
}
|
|
return $this->palette->getNext($type);
|
|
}
|
|
|
|
/**
|
|
* Render a row of pies
|
|
*
|
|
* @param Canvas $innerBox The canvas to insert the pies to
|
|
* @param DOMElement $labelBox The DOMElement to use for adding label elements
|
|
*/
|
|
private function renderPieRow(Canvas $innerBox, DOMElement $labelBox)
|
|
{
|
|
$radius = 50 / count($this->pies);
|
|
$x = $radius;
|
|
foreach ($this->pies as $pie) {
|
|
$labelPos = 0;
|
|
$lastRadius = 0;
|
|
|
|
foreach ($pie['data'] as $idx => $dataset) {
|
|
$slice = new PieSlice($radius, $dataset, $lastRadius);
|
|
$slice->setX($x)
|
|
->setStrokeColor('#000')
|
|
->setStrokeWidth(1)
|
|
->setY(50)
|
|
->setFill($this->getColorForPieSlice($pie, $idx));
|
|
$innerBox->addElement($slice);
|
|
// add caption if not disabled
|
|
if (!$this->noCaption && isset($pie['labels'])) {
|
|
$slice->setCaption($pie['labels'][$labelPos++])
|
|
->setLabelGroup($labelBox);
|
|
|
|
}
|
|
$lastRadius += $dataset;
|
|
}
|
|
// shift right for next pie
|
|
$x += $radius*2;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render pies in a stacked way so one pie is nested in the previous pie
|
|
*
|
|
* @param Canvas $innerBox The canvas to insert the pie to
|
|
* @param DOMElement $labelBox The DOMElement to use for adding label elements
|
|
*/
|
|
private function renderStackedPie(Canvas $innerBox, DOMElement $labelBox)
|
|
{
|
|
$radius = 40;
|
|
$minRadius = 20;
|
|
if (count($this->pies) == 0) {
|
|
return;
|
|
}
|
|
$shrinkStep = ($radius - $minRadius) / count($this->pies);
|
|
$x = $radius;
|
|
|
|
for ($i = 0; $i < count($this->pies); $i++) {
|
|
$pie = $this->pies[$i];
|
|
// the offset for the caption path, outer caption indicator shouldn't point
|
|
// to the middle of the slice as there will be another pie
|
|
$offset = isset($this->pies[$i+1]) ? $radius - $shrinkStep : 0;
|
|
$labelPos = 0;
|
|
$lastRadius = 0;
|
|
foreach ($pie['data'] as $idx => $dataset) {
|
|
$color = $this->getColorForPieSlice($pie, $idx);
|
|
if ($dataset === 100) {
|
|
$dataset = 99.9;
|
|
}
|
|
if ($dataset == 0) {
|
|
$labelPos++;
|
|
continue;
|
|
}
|
|
$slice = new PieSlice($radius, $dataset, $lastRadius);
|
|
$slice->setY(50)
|
|
->setX($x)
|
|
->setStrokeColor('#000')
|
|
->setStrokeWidth(1)
|
|
->setFill($color)
|
|
->setLabelGroup($labelBox);
|
|
|
|
if (!$this->noCaption && isset($pie['labels'])) {
|
|
$slice->setCaption($pie['labels'][$labelPos++])
|
|
->setCaptionOffset($offset)
|
|
->setOuterCaptionBound(50);
|
|
}
|
|
$innerBox->addElement($slice);
|
|
$lastRadius += $dataset;
|
|
}
|
|
// shrinken the next pie
|
|
$radius -= $shrinkStep;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the composition type of this PieChart
|
|
*
|
|
* @param string $type Either self::STACKED or self::ROW
|
|
*
|
|
* @return self Fluent interface
|
|
*/
|
|
public function setType($type)
|
|
{
|
|
$this->type = $type;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Hide the caption from this PieChart
|
|
*
|
|
* @return self Fluent interface
|
|
*/
|
|
public function disableLegend()
|
|
{
|
|
$this->noCaption = true;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Create the content for this PieChart
|
|
*
|
|
* @param Canvas $innerBox The innerbox to add the clip mask to
|
|
*/
|
|
private function createContentClipBox(Canvas $innerBox)
|
|
{
|
|
$clipBox = new Canvas('clip', new LayoutBox(0, 0, 100, 100));
|
|
$clipBox->toClipPath();
|
|
$innerBox->addElement($clipBox);
|
|
$rect = new Rect(0.1, 0, 100, 99.9);
|
|
$clipBox->addElement($rect);
|
|
}
|
|
}
|