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); } }