summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Chart/PieChart.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Chart/PieChart.php')
-rw-r--r--library/Icinga/Chart/PieChart.php306
1 files changed, 306 insertions, 0 deletions
diff --git a/library/Icinga/Chart/PieChart.php b/library/Icinga/Chart/PieChart.php
new file mode 100644
index 0000000..1bcf380
--- /dev/null
+++ b/library/Icinga/Chart/PieChart.php
@@ -0,0 +1,306 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+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 further 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;
+
+ public function __construct()
+ {
+ $this->title = t('Pie Chart');
+ $this->description = t('Contains data in a pie chart.');
+ parent::__construct();
+ }
+
+ /**
+ * 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 $this 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)
+ {
+ $labelBox = $ctx->getDocument()->createElement('g');
+ if (!$this->noCaption) {
+ // Scale SVG to make room for captions
+ $outerBox = new Canvas('outerGraph', new LayoutBox(33, -5, 40, 40));
+ $innerBox = new Canvas('graph', new LayoutBox(0, 0, 100, 100));
+ $innerBox->getLayout()->setPadding(10, 10, 10, 10);
+ } else {
+ $outerBox = new Canvas('outerGraph', new LayoutBox(1.5, -10, 124, 124));
+ $innerBox = new Canvas('graph', new LayoutBox(0, 0, 100, 100));
+ $innerBox->getLayout()->setPadding(0, 0, 0, 0);
+ }
+ $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 == 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 $this Fluent interface
+ */
+ public function setType($type)
+ {
+ $this->type = $type;
+ return $this;
+ }
+
+ /**
+ * Hide the caption from this PieChart
+ *
+ * @return $this 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);
+ }
+}