* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 * @author Icinga Development Team */ // {{{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; } 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 = 50; $minRadius = 20; $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) { $slice = new PieSlice($radius, $dataset, $lastRadius); $slice->setY(50) ->setX($x) ->setStrokeColor('#000') ->setStrokeWidth(1) ->setFill($this->getColorForPieSlice($pie, $idx)) ->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); } }