icingaweb2/library/Icinga/Chart/PieChart.php

322 lines
10 KiB
PHP

<?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga 2 Web.
*
* Icinga 2 Web - 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;
/**
* 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) ? 1 : count($this->pies), 1);
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);
}
}