Overview

Namespaces

  • SamChristy
    • PieChart

Classes

  • PieChart
  • PieChartColor
  • PieChartGD
  • PieChartImagick
  • Overview
  • Namespace
  • Class
  • Tree
  1: <?php
  2: namespace SamChristy\PieChart;
  3: include 'PieChart.php';
  4: 
  5: /**
  6:  * A lightweight class for drawing pie charts, using the GD library.
  7:  * @author    Sam Christy <sam_christy@hotmail.co.uk>
  8:  * @licence   GNU GPL v3.0 <http://www.gnu.org/licenses/gpl-3.0.html>
  9:  * @copyright © Sam Christy 2013
 10:  * @package   PieChart
 11:  * @version   v2.0.0
 12:  */
 13: class PieChartGD extends PieChart {
 14:     public function destroy() {
 15:         imageDestroy($this->canvas);
 16:     }
 17: 
 18:     /**
 19:      * Draws the pie chart, with optional supersampled anti-aliasing.
 20:      * @param int $aa
 21:      */
 22:     public function draw($aa = 4) {
 23:         $this->canvas = imageCreateTrueColor($this->width, $this->height);
 24: 
 25:         // Set anti-aliasing for the pie chart.
 26:         imageAntiAlias($this->canvas, true);
 27: 
 28:         imageFilledRectangle($this->canvas, 0, 0, $this->width, $this->height,
 29:                 $this->_convertColor($this->backgroundColor));
 30:         
 31:         $total = 0;
 32:         $sliceStart = -90;  // Start at 12 o'clock.
 33: 
 34:         $titleHeight = $this->_drawTitle();
 35:         $legendWidth = $this->_drawLegend($titleHeight);
 36: 
 37:         // Account for the space occupied by the legend and its padding.
 38:         $pieCentreX = ($this->width - $legendWidth) / 2;
 39: 
 40:         // Account for the space occupied by the title.
 41:         $pieCentreY = $titleHeight + ($this->height - $titleHeight) / 2;
 42: 
 43:         // 10% padding on the top and bottom of the pie.
 44:         $pieDiameter = round(
 45:                 min($this->width - $legendWidth, $this->height - $titleHeight) * 0.85
 46:         );
 47:         
 48:         foreach ($this->slices as $slice)
 49:             $total += $slice['value'];
 50:         
 51:         // If anti-aliasing is enabled, we supersample the pie to work around
 52:         // the fact that GD does not provide anti-aliasing natively.
 53:         if ($aa > 0) {
 54:             $ssDiameter = $pieDiameter * $aa;
 55:             $ssCentreX = $ssCentreY = $ssDiameter / 2 ;
 56:             $superSample = imageCreateTrueColor($ssDiameter, $ssDiameter);
 57:             imageFilledRectangle($superSample, 0, 0, $ssDiameter, $ssDiameter,
 58:                 $this->_convertColor($this->backgroundColor));
 59:             
 60:             foreach ($this->slices as $slice) {
 61:                 $sliceWidth = 360 * $slice['value'] / $total;
 62: 
 63:                 // Skip slices that are too small to draw / be visible.
 64:                 if ($sliceWidth == 0)
 65:                     continue;
 66:                 
 67:                 $sliceEnd = $sliceStart + $sliceWidth;
 68: 
 69:                 imageFilledArc(
 70:                     $superSample,
 71:                     $ssCentreX,
 72:                     $ssCentreY,
 73:                     $ssDiameter,
 74:                     $ssDiameter,
 75:                     $sliceStart,
 76:                     $sliceEnd,
 77:                     $this->_convertColor($slice['color']),
 78:                     IMG_ARC_PIE
 79:                 );
 80: 
 81:                 // Move along to the next slice.
 82:                 $sliceStart = $sliceEnd;
 83:             }
 84:             
 85:             imageCopyResampled(
 86:                 $this->canvas, $superSample,
 87:                 $pieCentreX - $pieDiameter / 2, $pieCentreY - $pieDiameter / 2,
 88:                 0, 0,
 89:                 $pieDiameter, $pieDiameter,
 90:                 $ssDiameter, $ssDiameter
 91:             );
 92:             
 93:             imageDestroy($superSample);
 94:         }
 95:         else {
 96:             // Draw the slices.
 97:             foreach ($this->slices as $slice) {
 98:                 $sliceWidth = 360 * $slice['value'] / $total;
 99: 
100:                 // Skip slices that are too small to draw / be visible.
101:                 if ($sliceWidth == 0)
102:                     continue;
103: 
104:                 $sliceEnd = $sliceStart + $sliceWidth;
105: 
106:                 imageFilledArc(
107:                     $this->canvas,
108:                     $pieCentreX,
109:                     $pieCentreY,
110:                     $pieDiameter,
111:                     $pieDiameter,
112:                     $sliceStart,
113:                     $sliceEnd,
114:                     $this->_convertColor($slice['color']),
115:                     IMG_ARC_PIE
116:                 );
117: 
118:                 // Move along to the next slice.
119:                 $sliceStart = $sliceEnd;
120:             }
121:         }
122:     }
123:     
124:     protected function _output($method, $format, $filename) {
125:         switch ($format) {
126:             case parent::FORMAT_GIF:
127:                 if ($method == parent::OUTPUT_INLINE || $method == parent::OUTPUT_DOWNLOAD) {
128:                     return imageGIF($this->canvas);
129:                 }
130:                 else if ($method == parent::OUTPUT_SAVE) {
131:                     return imageGIF($this->canvas, $filename);
132:                 }
133:                 break;
134:                 
135:             case parent::FORMAT_JPEG:
136:                 if ($method == parent::OUTPUT_INLINE || $method == parent::OUTPUT_DOWNLOAD) {
137:                     return imageJPEG($this->canvas, NULL, $this->quality);
138:                 }
139:                 else if ($method == parent::OUTPUT_SAVE) {
140:                     return imageJPEG($this->canvas, $filename, $this->quality);
141:                 }
142:                 break;
143:             
144:             case parent::FORMAT_PNG:
145:                 if ($method == parent::OUTPUT_INLINE || $method == parent::OUTPUT_DOWNLOAD) {
146:                     return imagePNG($this->canvas);
147:                 }
148:                 else if ($method == parent::OUTPUT_SAVE) {
149:                     return imagePNG($this->canvas, $filename);
150:                 }
151:                 break;
152:         }
153:         
154:         return false;  // The output method or format is missing!
155:     }
156: 
157:     /**
158:      * Draws the legend for the pieChart, if $this->hasLegend is true.
159:      * @param int $legendOffset The number of pixels the legend is offset by the title.
160:      * @return int The width of the legend and its padding.
161:      */
162:     protected function _drawLegend($legendOffset) {
163:         if (!$this->hasLegend)
164:             return 0;
165: 
166:         // Determine the ideal font size for the legend text;
167:         $legendFontSize = $this->width * 0.022;
168: 
169:         // If the legend's font size is too small, we won't bother drawing it.
170:         if (ceil($legendFontSize) < 8)
171:             return 0;
172: 
173:         // Calculate the size and padding for the color squares.
174:         $squareSize    = $this->height * 0.060;
175:         $squarePadding = $this->height * 0.025;
176:         $labelPadding  = $this->height * 0.025;
177: 
178:         $sliceCount = count($this->slices);
179: 
180:         $legendPadding = 0.05 * $this->width;
181: 
182:         // Determine the width and height of the legend.
183:         $legendWidth = $squareSize + $labelPadding + $this->_maxLabelWidth($legendFontSize);
184:         $legendHeight = $sliceCount * ($squareSize + $squarePadding) - $squarePadding;
185: 
186:         // If the legend and its padding occupy too much space, we will not draw it.        
187:         if ($legendWidth + $legendPadding * 2 > $this->width / 2)  // Too wide.
188:             return 0;
189: 
190:         if ($legendHeight > $this->height - $legendOffset - $legendPadding * 2)  // Too high.
191:             return 0;
192: 
193:         $legendX = $this->width - $legendWidth - $legendPadding;
194:         $legendY = ($this->height - $legendOffset) / 2 + $legendOffset - $legendHeight / 2;
195: 
196:         $i = 0;
197:         foreach ($this->slices as $sliceName => $slice) {
198:             // Move down...
199:             $OffsetY = $i++ * ($squareSize + $squarePadding);
200: 
201:             $this->_drawLegendKey(
202:                 $legendX,
203:                 $legendY + $OffsetY,
204:                 $slice['color'],
205:                 $sliceName,
206:                 $squareSize,
207:                 $labelPadding,
208:                 $legendFontSize
209:             );
210:         }
211: 
212:         return $legendWidth + $legendPadding * 2;
213:     }
214: 
215:     /**
216:      * Draws the legend key at the specific location.
217:      * @param int $x The x coordinate for the key's top, left corner.
218:      * @param int $y The y coordinate for the key's top, left corner.
219:      * @param object $color The GD colour identifier, created with imageColorAllocate().
220:      * @param string $label
221:      * @param int $squareSize The size of the square, in pixels.
222:      * @param int $labelPadding
223:      * @param int $fontSize
224:      */
225:     protected function _drawLegendKey($x, $y, $color, $label, $squareSize, $labelPadding,
226:             $fontSize) {
227:         $labelX = $x + $squareSize + $labelPadding;
228: 
229:         // Centre the label vertically to the square.
230:         $labelBBox = imageTTFBBox($fontSize, 0, $this->legendFont, $label);
231:         $labelHeight = abs($labelBBox[7] - $labelBBox[1]);
232: 
233:         $labelY = $y + $squareSize / 2 - $labelHeight / 2;
234:         
235:         imageFilledRectangle(
236:            $this->canvas, $x, $y, $x + $squareSize, $y + $squareSize, $this->_convertColor($color)
237:         );
238: 
239:         imageTTFText(
240:             $this->canvas,
241:             $fontSize,
242:             0,
243:             $labelX + abs($labelBBox[0]), // Eliminate left overhang.
244:             $labelY + abs($labelBBox[7]), // Eliminate area above the baseline.
245:             $this->_convertColor($this->textColor),
246:             $this->legendFont,
247:             $label
248:         );
249:     }
250: 
251:     /**
252:      * Returns the width, in pixels, of the chart's widest label.
253:      * @return int
254:      */
255:     protected function _maxLabelWidth($fontSize) {
256:         $widestLabelWidth = 0;
257: 
258:         foreach ($this->slices as $sliceName => $slice) {
259:             // Measure the label.
260:             $boundingBox = imageTTFBBox($fontSize, 0, $this->legendFont, $sliceName);
261:             $labelWidth = $boundingBox[2] - $boundingBox[0];
262: 
263:             if ($labelWidth > $widestLabelWidth)
264:                 $widestLabelWidth = $labelWidth;
265:         }
266: 
267:         return $widestLabelWidth;
268:     }
269: 
270:     /**
271:      * Draws and returns the height of the title and its padding (in pixels). If no title is 
272:      * specified, then nothing is drawn and 0 is returned.
273:      * @var float x location
274:      * @var float y location
275:      * @return int The height of the title + padding.
276:      */
277:     protected function _drawTitle($x = 0, $y = 0) {
278:         if (!$this->title)
279:             return 0;
280: 
281:         $titleColor = $this->_convertColor($this->textColor);
282: 
283:         // Determine ideal font size for the title.
284:         $titleSize = 0.0675 * $this->height;  // The largest sensible value.
285:         $minTitleSize = 10;                   // The smallest legible value.
286: 
287:         do {
288:             $titleBBox = imageTTFBBox($titleSize, 0, $this->titleFont, $this->title);
289:             $titleWidth = $titleBBox[2] - $titleBBox[0];
290: 
291:             // If we can fit the title in, with 5% padding on each side, then we can 
292:             // draw it.
293:             if ($titleWidth <= ($this->width * 0.9))
294:                 break;
295: 
296:             $titleSize -= 0.5; // Try a smaller font size.
297:         } while ($titleSize >= $minTitleSize);
298: 
299:         // If the title is simply too long to be drawn legibly, then we will simply not 
300:         // draw it.
301:         if ($titleSize < $minTitleSize)
302:             return 0;
303: 
304:         $titleHeight = abs($titleBBox[7] - $titleBBox[1]);
305: 
306:         // Give the title 7.5% top padding.
307:         $titleTopPadding = 0.075 * $this->height;
308: 
309:         // Centre the title.
310:         $x = $this->width / 2 - $titleWidth / 2;
311:         $y = $titleTopPadding;
312: 
313:         imageTtfText(
314:             $this->canvas, $titleSize, 
315:             0,
316:             $x + abs($titleBBox[0]),  // Account for left overhang.
317:             $y + abs($titleBBox[7]),  // Account for the area above the baseline.
318:             $titleColor, 
319:             $this->titleFont, 
320:             $this->title
321:         );
322: 
323:         return $titleHeight + $titleTopPadding;
324:     }
325:     
326:     /**
327:      * A convenience function for converting PieChartColor objects to the format
328:      * that GD requires.
329:      */
330:     private function _convertColor(PieChartColor $color) {
331:         // Interestingly, GD uses the ARGB format internally, so 
332:         // PieChartColor::toInt() would actually work for everything but GIFs...
333:         return imageColorAllocate($this->canvas, $color->r, $color->g, $color->b);
334:     }
335: }
API documentation generated by ApiGen 2.8.0