Matthias Jentsch 2360f21b09 Fix InlinePie boundaries and use floats for the PieSlice path coordinates
Use floats as path coordinates in PieSlices, to
avoid that the start and ending position of the arc are at the exact same
position. This would cause buggy behavior, when displaying values like "99,999%".

refs #5863
2014-03-26 17:06:21 +01:00

368 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');
if (!$this->noCaption) {
$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);
}
}