1<?php
2namespace Amenadiel\JpGraph\Plot;
3
4use Amenadiel\JpGraph\Graph;
5use Amenadiel\JpGraph\Text;
6use Amenadiel\JpGraph\Util;
7
8/*=======================================================================
9// File:        JPGRAPH_PIE.PHP
10// Description: Pie plot extension for JpGraph
11// Created:     2001-02-14
12// Ver:         $Id: jpgraph_pie.php 1926 2010-01-11 16:33:07Z ljp $
13//
14// Copyright (c) Asial Corporation. All rights reserved.
15//========================================================================
16 */
17
18// Defines for PiePlot::SetLabelType()
19define("PIE_VALUE_ABS", 1);
20define("PIE_VALUE_PER", 0);
21define("PIE_VALUE_PERCENTAGE", 0);
22define("PIE_VALUE_ADJPERCENTAGE", 2);
23define("PIE_VALUE_ADJPER", 2);
24
25//===================================================
26// CLASS PiePlot
27// Description: Draws a pie plot
28//===================================================
29class PiePlot
30{
31    public $posx                        = 0.5;
32    public $posy                        = 0.5;
33    public $is_using_plot_theme         = false;
34    public $theme                       = "earth";
35    protected $use_plot_theme_colors    = false;
36    protected $radius                   = 0.3;
37    protected $explode_radius           = array();
38    protected $explode_all              = false;
39    protected $explode_r                = 20;
40    protected $labels                   = null;
41    protected $legends                  = null;
42    protected $csimtargets              = null;
43    protected $csimwintargets           = null; // Array of targets for CSIM
44    protected $csimareas                = ''; // Generated CSIM text
45    protected $csimalts                 = null; // ALT tags for corresponding target
46    protected $data                     = null;
47    public $title;
48    protected $startangle    = 0;
49    protected $weight        = 1;
50    protected $color         = "black";
51    protected $legend_margin = 6;
52    protected $show_labels   = true;
53    protected $themearr      = array(
54        "earth"  => array(136, 34, 40, 45, 46, 62, 63, 134, 74, 10, 120, 136, 141, 168, 180, 77, 209, 218, 346, 395, 89, 430),
55        "pastel" => array(27, 415, 128, 59, 66, 79, 105, 110, 42, 147, 152, 230, 236, 240, 331, 337, 405, 38),
56        "water"  => array(8, 370, 24, 40, 335, 56, 213, 237, 268, 14, 326, 387, 10, 388),
57        "sand"   => array(27, 168, 34, 170, 19, 50, 65, 72, 131, 209, 46, 393));
58    protected $setslicecolors          = array();
59    protected $labeltype               = 0; // Default to percentage
60    protected $pie_border              = true;
61    protected $pie_interior_border     = true;
62    public $value;
63    protected $ishadowcolor              = '';
64    protected $ishadowdrop               = 4;
65    protected $ilabelposadj              = 1;
66    protected $legendcsimtargets         = array();
67    protected $legendcsimwintargets      = array();
68    protected $legendcsimalts            = array();
69    protected $adjusted_data             = array();
70    public $guideline                    = null;
71    protected $guidelinemargin           = 10;
72    protected $iShowGuideLineForSingle   = false;
73    protected $iGuideLineCurve           = false;
74    protected $iGuideVFactor             = 1.4;
75    protected $iGuideLineRFactor         = 0.8;
76    protected $la                        = array(); // Holds the exact angle for each label
77
78    //---------------
79    // CONSTRUCTOR
80    public function __construct($data)
81    {
82        $this->data  = array_reverse($data);
83        $this->title = new Text\Text("");
84        $this->title->SetFont(FF_DEFAULT, FS_BOLD);
85        $this->value = new DisplayValue();
86        $this->value->Show();
87        $this->value->SetFormat('%.1f%%');
88        $this->guideline = new Graph\LineProperty();
89    }
90
91    //---------------
92    // PUBLIC METHODS
93    public function SetCenter($x, $y = 0.5)
94    {
95        $this->posx = $x;
96        $this->posy = $y;
97    }
98
99    // Enable guideline and set drwaing policy
100    public function SetGuideLines($aFlg = true, $aCurved = true, $aAlways = false)
101    {
102        $this->guideline->Show($aFlg);
103        $this->iShowGuideLineForSingle = $aAlways;
104        $this->iGuideLineCurve         = $aCurved;
105    }
106
107    // Adjuste the distance between labels and labels and pie
108    public function SetGuideLinesAdjust($aVFactor, $aRFactor = 0.8)
109    {
110        $this->iGuideVFactor     = $aVFactor;
111        $this->iGuideLineRFactor = $aRFactor;
112    }
113
114    public function SetColor($aColor)
115    {
116        $this->color = $aColor;
117    }
118
119    public function SetSliceColors($aColors)
120    {
121        $this->setslicecolors = $aColors;
122    }
123
124    public function SetShadow($aColor = 'darkgray', $aDropWidth = 4)
125    {
126        $this->ishadowcolor = $aColor;
127        $this->ishadowdrop  = $aDropWidth;
128    }
129
130    public function SetCSIMTargets($aTargets, $aAlts = '', $aWinTargets = '')
131    {
132        $this->csimtargets = array_reverse($aTargets);
133        if (is_array($aWinTargets)) {
134            $this->csimwintargets = array_reverse($aWinTargets);
135        }
136
137        if (is_array($aAlts)) {
138            $this->csimalts = array_reverse($aAlts);
139        }
140    }
141
142    public function GetCSIMareas()
143    {
144        return $this->csimareas;
145    }
146
147    public function AddSliceToCSIM($i, $xc, $yc, $radius, $sa, $ea)
148    {
149        //Slice number, ellipse centre (x,y), height, width, start angle, end angle
150        while ($sa > 2 * M_PI) {
151            $sa = $sa - 2 * M_PI;
152        }
153
154        while ($ea > 2 * M_PI) {
155            $ea = $ea - 2 * M_PI;
156        }
157
158        $sa = 2 * M_PI - $sa;
159        $ea = 2 * M_PI - $ea;
160
161        // Special case when we have only one slice since then both start and end
162        // angle will be == 0
163        if (abs($sa - $ea) < 0.0001) {
164            $sa = 2 * M_PI;
165            $ea = 0;
166        }
167
168        //add coordinates of the centre to the map
169        $xc     = floor($xc);
170        $yc     = floor($yc);
171        $coords = "$xc, $yc";
172
173        //add coordinates of the first point on the arc to the map
174        $xp = floor(($radius * cos($ea)) + $xc);
175        $yp = floor($yc - $radius * sin($ea));
176        $coords .= ", $xp, $yp";
177
178        //add coordinates every 0.2 radians
179        $a = $ea + 0.2;
180
181        // If we cross the 360-limit with a slice we need to handle
182        // the fact that end angle is smaller than start
183        if ($sa < $ea) {
184            while ($a <= 2 * M_PI) {
185                $xp = floor($radius * cos($a) + $xc);
186                $yp = floor($yc - $radius * sin($a));
187                $coords .= ", $xp, $yp";
188                $a += 0.2;
189            }
190            $a -= 2 * M_PI;
191        }
192
193        while ($a < $sa) {
194            $xp = floor($radius * cos($a) + $xc);
195            $yp = floor($yc - $radius * sin($a));
196            $coords .= ", $xp, $yp";
197            $a += 0.2;
198        }
199
200        //Add the last point on the arc
201        $xp = floor($radius * cos($sa) + $xc);
202        $yp = floor($yc - $radius * sin($sa));
203        $coords .= ", $xp, $yp";
204        if (!empty($this->csimtargets[$i])) {
205            $this->csimareas .= "<area shape=\"poly\" coords=\"$coords\" href=\"" . $this->csimtargets[$i] . "\"";
206            $tmp = "";
207            if (!empty($this->csimwintargets[$i])) {
208                $this->csimareas .= " target=\"" . $this->csimwintargets[$i] . "\" ";
209            }
210            if (!empty($this->csimalts[$i])) {
211                $tmp = sprintf($this->csimalts[$i], $this->data[$i]);
212                $this->csimareas .= " title=\"$tmp\" alt=\"$tmp\" ";
213            }
214            $this->csimareas .= " />\n";
215        }
216    }
217
218    public function SetTheme($aTheme)
219    {
220        //        Util\JpGraphError::RaiseL(15012,$aTheme);
221        //        return;
222
223        if (in_array($aTheme, array_keys($this->themearr))) {
224            $this->theme               = $aTheme;
225            $this->is_using_plot_theme = true;
226        } else {
227            Util\JpGraphError::RaiseL(15001, $aTheme); //("PiePLot::SetTheme() Unknown theme: $aTheme");
228        }
229    }
230
231    public function ExplodeSlice($e, $radius = 20)
232    {
233        if (!is_integer($e)) {
234            Util\JpGraphError::RaiseL(15002);
235        }
236        //('Argument to PiePlot::ExplodeSlice() must be an integer');
237        $this->explode_radius[$e] = $radius;
238    }
239
240    public function ExplodeAll($radius = 20)
241    {
242        $this->explode_all = true;
243        $this->explode_r   = $radius;
244    }
245
246    public function Explode($aExplodeArr)
247    {
248        if (!is_array($aExplodeArr)) {
249            Util\JpGraphError::RaiseL(15003);
250            //("Argument to PiePlot::Explode() must be an array with integer distances.");
251        }
252        $this->explode_radius = $aExplodeArr;
253    }
254
255    public function SetStartAngle($aStart)
256    {
257        if ($aStart < 0 || $aStart > 360) {
258            Util\JpGraphError::RaiseL(15004); //('Slice start angle must be between 0 and 360 degrees.');
259        }
260        if ($aStart == 0) {
261            $this->startangle = 0;
262        } else {
263            $this->startangle = 360 - $aStart;
264            $this->startangle *= M_PI / 180;
265        }
266    }
267
268    // Size in percentage
269    public function SetSize($aSize)
270    {
271        if (($aSize > 0 && $aSize <= 0.5) || ($aSize > 10 && $aSize < 1000)) {
272            $this->radius = $aSize;
273        } else {
274            Util\JpGraphError::RaiseL(15006);
275        }
276
277        //("PiePlot::SetSize() Radius for pie must either be specified as a fraction [0, 0.5] of the size of the image or as an absolute size in pixels  in the range [10, 1000]");
278    }
279
280    // Set label arrays
281    public function SetLegends($aLegend)
282    {
283        $this->legends = $aLegend;
284    }
285
286    // Set text labels for slices
287    public function SetLabels($aLabels, $aLblPosAdj = "auto")
288    {
289        $this->labels       = array_reverse($aLabels);
290        $this->ilabelposadj = $aLblPosAdj;
291    }
292
293    public function SetLabelPos($aLblPosAdj)
294    {
295        $this->ilabelposadj = $aLblPosAdj;
296    }
297
298    // Should we display actual value or percentage?
299    public function SetLabelType($aType)
300    {
301        if ($aType < 0 || $aType > 2) {
302            Util\JpGraphError::RaiseL(15008, $aType);
303        }
304
305        //("PiePlot::SetLabelType() Type for pie plots must be 0 or 1 (not $t).");
306        $this->labeltype = $aType;
307    }
308
309    // Deprecated.
310    public function SetValueType($aType)
311    {
312        $this->SetLabelType($aType);
313    }
314
315    // Should the circle around a pie plot be displayed
316    public function ShowBorder($exterior = true, $interior = true)
317    {
318        $this->pie_border          = $exterior;
319        $this->pie_interior_border = $interior;
320    }
321
322    // Setup the legends
323    public function Legend($graph)
324    {
325        $colors = array_keys($graph->img->rgb->rgb_table);
326        sort($colors);
327        $ta = $this->themearr[$this->theme];
328        $n  = count($this->data);
329
330        if ($this->setslicecolors == null) {
331            $numcolors = count($ta);
332            if (class_exists('PiePlot3D', false) && ($this instanceof PiePlot3D)) {
333                $ta = array_reverse(array_slice($ta, 0, $n));
334            }
335        } else {
336            $this->setslicecolors = array_slice($this->setslicecolors, 0, $n);
337            $numcolors            = count($this->setslicecolors);
338            if ($graph->pieaa && !($this instanceof PiePlot3D)) {
339                $this->setslicecolors = array_reverse($this->setslicecolors);
340            }
341        }
342
343        $sum = 0;
344        for ($i = 0; $i < $n; ++$i) {
345            $sum += $this->data[$i];
346        }
347
348        // Bail out with error if the sum is 0
349        if ($sum == 0) {
350            Util\JpGraphError::RaiseL(15009);
351        }
352        //("Illegal pie plot. Sum of all data is zero for Pie!");
353
354        // Make sure we don't plot more values than data points
355        // (in case the user added more legends than data points)
356        $n = min(count($this->legends), count($this->data));
357        if ($this->legends != "") {
358            $this->legends = array_reverse(array_slice($this->legends, 0, $n));
359        }
360        for ($i = $n - 1; $i >= 0; --$i) {
361            $l = $this->legends[$i];
362            // Replace possible format with actual values
363            if (count($this->csimalts) > $i) {
364                $fmt = $this->csimalts[$i];
365            } else {
366                $fmt = "%d"; // Deafult Alt if no other has been specified
367            }
368            if ($this->labeltype == 0) {
369                $l   = sprintf($l, 100 * $this->data[$i] / $sum);
370                $alt = sprintf($fmt, $this->data[$i]);
371            } elseif ($this->labeltype == 1) {
372                $l   = sprintf($l, $this->data[$i]);
373                $alt = sprintf($fmt, $this->data[$i]);
374            } else {
375                $l   = sprintf($l, $this->adjusted_data[$i]);
376                $alt = sprintf($fmt, $this->adjusted_data[$i]);
377            }
378
379            if (empty($this->csimwintargets[$i])) {
380                $wintarg = '';
381            } else {
382                $wintarg = $this->csimwintargets[$i];
383            }
384
385            if ($this->setslicecolors == null) {
386                $graph->legend->Add($l, $colors[$ta[$i % $numcolors]], "", 0, $this->csimtargets[$i], $alt, $wintarg);
387            } else {
388                $graph->legend->Add($l, $this->setslicecolors[$i % $numcolors], "", 0, $this->csimtargets[$i], $alt, $wintarg);
389            }
390        }
391    }
392
393    // Adjust the rounded percetage value so that the sum of
394    // of the pie slices are always 100%
395    // Using the Hare/Niemeyer method
396    public function AdjPercentage($aData, $aPrec = 0)
397    {
398        $mul = 100;
399        if ($aPrec > 0 && $aPrec < 3) {
400            if ($aPrec == 1) {
401                $mul = 1000;
402            } else {
403                $mul = 10000;
404            }
405        }
406
407        $tmp       = array();
408        $result    = array();
409        $quote_sum = 0;
410        $n         = count($aData);
411        for ($i = 0, $sum = 0; $i < $n; ++$i) {
412            $sum += $aData[$i];
413        }
414
415        foreach ($aData as $index => $value) {
416            $tmp_percentage = $value / $sum * $mul;
417            $result[$index] = floor($tmp_percentage);
418            $tmp[$index]    = $tmp_percentage - $result[$index];
419            $quote_sum += $result[$index];
420        }
421        if ($quote_sum == $mul) {
422            if ($mul > 100) {
423                $tmp = $mul / 100;
424                for ($i = 0; $i < $n; ++$i) {
425                    $result[$i] /= $tmp;
426                }
427            }
428            return $result;
429        }
430        arsort($tmp, SORT_NUMERIC);
431        reset($tmp);
432        for ($i = 0; $i < $mul - $quote_sum; $i++) {
433            $result[key($tmp)]++;
434            next($tmp);
435        }
436        if ($mul > 100) {
437            $tmp = $mul / 100;
438            for ($i = 0; $i < $n; ++$i) {
439                $result[$i] /= $tmp;
440            }
441        }
442        return $result;
443    }
444
445    public function Stroke($img, $aaoption = 0)
446    {
447        // aaoption is used to handle antialias
448        // aaoption == 0 a normal pie
449        // aaoption == 1 just the body
450        // aaoption == 2 just the values
451
452        // Explode scaling. If anti alias we scale the image
453        // twice and we also need to scale the exploding distance
454        $expscale = $aaoption === 1 ? 2 : 1;
455
456        if ($this->labeltype == 2) {
457            // Adjust the data so that it will add up to 100%
458            $this->adjusted_data = $this->AdjPercentage($this->data);
459        }
460
461        if ($this->use_plot_theme_colors) {
462            $this->setslicecolors = null;
463        }
464
465        $colors = array_keys($img->rgb->rgb_table);
466        sort($colors);
467        $ta = $this->themearr[$this->theme];
468        $n  = count($this->data);
469
470        if ($this->setslicecolors == null) {
471            $numcolors = count($ta);
472        } else {
473            // We need to create an array of colors as long as the data
474            // since we need to reverse it to get the colors in the right order
475            $numcolors = count($this->setslicecolors);
476            $i         = 2 * $numcolors;
477            while ($n > $i) {
478                $this->setslicecolors = array_merge($this->setslicecolors, $this->setslicecolors);
479                $i += $n;
480            }
481            $tt                   = array_slice($this->setslicecolors, 0, $n % $numcolors);
482            $this->setslicecolors = array_merge($this->setslicecolors, $tt);
483            $this->setslicecolors = array_reverse($this->setslicecolors);
484        }
485
486        // Draw the slices
487        $sum = 0;
488        for ($i = 0; $i < $n; ++$i) {
489            $sum += $this->data[$i];
490        }
491
492        // Bail out with error if the sum is 0
493        if ($sum == 0) {
494            Util\JpGraphError::RaiseL(15009); //("Sum of all data is 0 for Pie.");
495        }
496
497        // Set up the pie-circle
498        if ($this->radius <= 1) {
499            $radius = floor($this->radius * min($img->width, $img->height));
500        } else {
501            $radius = $aaoption === 1 ? $this->radius * 2 : $this->radius;
502        }
503
504        if ($this->posx <= 1 && $this->posx > 0) {
505            $xc = round($this->posx * $img->width);
506        } else {
507            $xc = $this->posx;
508        }
509
510        if ($this->posy <= 1 && $this->posy > 0) {
511            $yc = round($this->posy * $img->height);
512        } else {
513            $yc = $this->posy;
514        }
515
516        $n = count($this->data);
517
518        if ($this->explode_all) {
519            for ($i = 0; $i < $n; ++$i) {
520                $this->explode_radius[$i] = $this->explode_r;
521            }
522        }
523
524        // If we have a shadow and not just drawing the labels
525        if ($this->ishadowcolor != "" && $aaoption !== 2) {
526            $accsum = 0;
527            $angle2 = $this->startangle;
528            $img->SetColor($this->ishadowcolor);
529            for ($i = 0; $sum > 0 && $i < $n; ++$i) {
530                $j      = $n - $i - 1;
531                $d      = $this->data[$i];
532                $angle1 = $angle2;
533                $accsum += $d;
534                $angle2 = $this->startangle + 2 * M_PI * $accsum / $sum;
535                if (empty($this->explode_radius[$j])) {
536                    $this->explode_radius[$j] = 0;
537                }
538
539                if ($d < 0.00001) {
540                    continue;
541                }
542
543                $la = 2 * M_PI - (abs($angle2 - $angle1) / 2.0 + $angle1);
544
545                $xcm = $xc + $this->explode_radius[$j] * cos($la) * $expscale;
546                $ycm = $yc - $this->explode_radius[$j] * sin($la) * $expscale;
547
548                $xcm += $this->ishadowdrop * $expscale;
549                $ycm += $this->ishadowdrop * $expscale;
550
551                $_sa = round($angle1 * 180 / M_PI);
552                $_ea = round($angle2 * 180 / M_PI);
553
554                // The CakeSlice method draws a full circle in case of start angle = end angle
555                // for pie slices we don't want this behaviour unless we only have one
556                // slice in the pie in case it is the wanted behaviour
557                if ($_ea - $_sa > 0.1 || $n == 1) {
558                    $img->CakeSlice($xcm, $ycm, $radius - 1, $radius - 1,
559                        $angle1 * 180 / M_PI, $angle2 * 180 / M_PI, $this->ishadowcolor);
560                }
561            }
562        }
563
564        //--------------------------------------------------------------------------------
565        // This is the main loop to draw each cake slice
566        //--------------------------------------------------------------------------------
567
568        // Set up the accumulated sum, start angle for first slice and border color
569        $accsum = 0;
570        $angle2 = $this->startangle;
571        $img->SetColor($this->color);
572
573        // Loop though all the slices if there is a pie to draw (sum>0)
574        // There are n slices in total
575        for ($i = 0; $sum > 0 && $i < $n; ++$i) {
576
577            // $j is the actual index used for the slice
578            $j = $n - $i - 1;
579
580            // Make sure we havea  valid distance to explode the slice
581            if (empty($this->explode_radius[$j])) {
582                $this->explode_radius[$j] = 0;
583            }
584
585            // The actual numeric value for the slice
586            $d = $this->data[$i];
587
588            $angle1 = $angle2;
589
590            // Accumlate the sum
591            $accsum += $d;
592
593            // The new angle when we add the "size" of this slice
594            // angle1 is then the start and angle2 the end of this slice
595            $angle2 = $this->NormAngle($this->startangle + 2 * M_PI * $accsum / $sum);
596
597            // We avoid some trouble by not allowing end angle to be 0, in that case
598            // we translate to 360
599
600            // la is used to hold the label angle, which is centered on the slice
601            if ($angle2 < 0.0001 && $angle1 > 0.0001) {
602                $this->la[$i] = 2 * M_PI - (abs(2 * M_PI - $angle1) / 2.0 + $angle1);
603            } elseif ($angle1 > $angle2) {
604                // The case where the slice crosses the 3 a'clock line
605                // Remember that the slices are counted clockwise and
606                // labels are counted counter clockwise so we need to revert with 2 PI
607                $this->la[$i] = 2 * M_PI - $this->NormAngle($angle1 + ((2 * M_PI - $angle1) + $angle2) / 2);
608            } else {
609                $this->la[$i] = 2 * M_PI - (abs($angle2 - $angle1) / 2.0 + $angle1);
610            }
611
612            // Too avoid rounding problems we skip the slice if it is too small
613            if ($d < 0.00001) {
614                continue;
615            }
616
617            // If the user has specified an array of colors for each slice then use
618            // that a color otherwise use the theme array (ta) of colors
619            if ($this->setslicecolors == null) {
620                $slicecolor = $colors[$ta[$i % $numcolors]];
621            } else {
622                $slicecolor = $this->setslicecolors[$i % $numcolors];
623            }
624
625            //            $_sa = round($angle1*180/M_PI);
626            //            $_ea = round($angle2*180/M_PI);
627            //            $_la = round($this->la[$i]*180/M_PI);
628            //            echo "Slice#$i: ang1=$_sa , ang2=$_ea, la=$_la, color=$slicecolor<br>";
629
630            // If we have enabled antialias then we don't draw any border so
631            // make the bordedr color the same as the slice color
632            if ($this->pie_interior_border && $aaoption === 0) {
633                $img->SetColor($this->color);
634            } else {
635                $img->SetColor($slicecolor);
636            }
637            $arccolor = $this->pie_border && $aaoption === 0 ? $this->color : "";
638
639            // Calculate the x,y coordinates for the base of this slice taking
640            // the exploded distance into account. Here we use the mid angle as the
641            // ray of extension and we have the mid angle handy as it is also the
642            // label angle
643            $xcm = $xc + $this->explode_radius[$j] * cos($this->la[$i]) * $expscale;
644            $ycm = $yc - $this->explode_radius[$j] * sin($this->la[$i]) * $expscale;
645
646            // If we are not just drawing the labels then draw this cake slice
647            if ($aaoption !== 2) {
648                $_sa = round($angle1 * 180 / M_PI);
649                $_ea = round($angle2 * 180 / M_PI);
650                $_la = round($this->la[$i] * 180 / M_PI);
651                //echo "[$i] sa=$_sa, ea=$_ea, la[$i]=$_la, (color=$slicecolor)<br>";
652
653                // The CakeSlice method draws a full circle in case of start angle = end angle
654                // for pie slices we want this in case the slice have a value larger than 99% of the
655                // total sum
656                if (abs($_ea - $_sa) >= 1 || $d == $sum) {
657                    $img->CakeSlice($xcm, $ycm, $radius - 1, $radius - 1, $_sa, $_ea, $slicecolor, $arccolor);
658                }
659            }
660
661            // If the CSIM is used then make sure we register a CSIM area for this slice as well
662            if ($this->csimtargets && $aaoption !== 1) {
663                $this->AddSliceToCSIM($i, $xcm, $ycm, $radius, $angle1, $angle2);
664            }
665        }
666
667        // Format the titles for each slice
668        if ($aaoption !== 2) {
669            for ($i = 0; $i < $n; ++$i) {
670                if ($this->labeltype == 0) {
671                    if ($sum != 0) {
672                        $l = 100.0 * $this->data[$i] / $sum;
673                    } else {
674                        $l = 0.0;
675                    }
676                } elseif ($this->labeltype == 1) {
677                    $l = $this->data[$i] * 1.0;
678                } else {
679                    $l = $this->adjusted_data[$i];
680                }
681                if (isset($this->labels[$i]) && is_string($this->labels[$i])) {
682                    $this->labels[$i] = sprintf($this->labels[$i], $l);
683                } else {
684                    $this->labels[$i] = $l;
685                }
686            }
687        }
688
689        if ($this->value->show && $aaoption !== 1) {
690            $this->StrokeAllLabels($img, $xc, $yc, $radius);
691        }
692
693        // Adjust title position
694        if ($aaoption !== 1) {
695            $this->title->SetPos($xc,
696                $yc - $this->title->GetFontHeight($img) - $radius - $this->title->margin,
697                "center", "bottom");
698            $this->title->Stroke($img);
699        }
700    }
701
702    //---------------
703    // PRIVATE METHODS
704
705    public function NormAngle($a)
706    {
707        while ($a < 0) {
708            $a += 2 * M_PI;
709        }
710
711        while ($a > 2 * M_PI) {
712            $a -= 2 * M_PI;
713        }
714
715        return $a;
716    }
717
718    public function Quadrant($a)
719    {
720        $a = $this->NormAngle($a);
721        if ($a > 0 && $a <= M_PI / 2) {
722            return 0;
723        }
724
725        if ($a > M_PI / 2 && $a <= M_PI) {
726            return 1;
727        }
728
729        if ($a > M_PI && $a <= 1.5 * M_PI) {
730            return 2;
731        }
732
733        if ($a > 1.5 * M_PI) {
734            return 3;
735        }
736    }
737
738    public function StrokeGuideLabels($img, $xc, $yc, $radius)
739    {
740        $n = count($this->labels);
741
742        //-----------------------------------------------------------------------
743        // Step 1 of the algorithm is to construct a number of clusters
744        // a cluster is defined as all slices within the same quadrant (almost)
745        // that has an angular distance less than the treshold
746        //-----------------------------------------------------------------------
747        $tresh_hold = 25 * M_PI / 180; // 25 degrees difference to be in a cluster
748        $incluster  = false; // flag if we are currently in a cluster or not
749        $clusters   = array(); // array of clusters
750        $cidx       = -1; // running cluster index
751
752        // Go through all the labels and construct a number of clusters
753        for ($i = 0; $i < $n - 1; ++$i) {
754            // Calc the angle distance between two consecutive slices
755            $a1   = $this->la[$i];
756            $a2   = $this->la[$i + 1];
757            $q1   = $this->Quadrant($a1);
758            $q2   = $this->Quadrant($a2);
759            $diff = abs($a1 - $a2);
760            if ($diff < $tresh_hold) {
761                if ($incluster) {
762                    $clusters[$cidx][1]++;
763                    // Each cluster can only cover one quadrant
764                    // Do we cross a quadrant ( and must break the cluster)
765                    if ($q1 != $q2) {
766                        // If we cross a quadrant boundary we normally start a
767                        // new cluster. However we need to take the 12'a clock
768                        // and 6'a clock positions into a special consideration.
769                        // Case 1: WE go from q=1 to q=2 if the last slice on
770                        // the cluster for q=1 is close to 12'a clock and the
771                        // first slice in q=0 is small we extend the previous
772                        // cluster
773                        if ($q1 == 1 && $q2 == 0 && $a2 > (90 - 15) * M_PI / 180) {
774                            if ($i < $n - 2) {
775                                $a3 = $this->la[$i + 2];
776                                // If there isn't a cluster coming up with the next-next slice
777                                // we extend the previous cluster to cover this slice as well
778                                if (abs($a3 - $a2) >= $tresh_hold) {
779                                    $clusters[$cidx][1]++;
780                                    $i++;
781                                }
782                            }
783                        } elseif ($q1 == 3 && $q2 == 2 && $a2 > (270 - 15) * M_PI / 180) {
784                            if ($i < $n - 2) {
785                                $a3 = $this->la[$i + 2];
786                                // If there isn't a cluster coming up with the next-next slice
787                                // we extend the previous cluster to cover this slice as well
788                                if (abs($a3 - $a2) >= $tresh_hold) {
789                                    $clusters[$cidx][1]++;
790                                    $i++;
791                                }
792                            }
793                        }
794
795                        if ($q1 == 2 && $q2 == 1 && $a2 > (180 - 15) * M_PI / 180) {
796                            $clusters[$cidx][1]++;
797                            $i++;
798                        }
799
800                        $incluster = false;
801                    }
802                } elseif ($q1 == $q2) {
803                    $incluster = true;
804                    // Now we have a special case for quadrant 0. If we previously
805                    // have a cluster of one in quadrant 0 we just extend that
806                    // cluster. If we don't do this then we risk that the label
807                    // for the cluster of one will cross the guide-line
808                    if ($q1 == 0 && $cidx > -1 &&
809                        $clusters[$cidx][1] == 1 &&
810                        $this->Quadrant($this->la[$clusters[$cidx][0]]) == 0) {
811                        $clusters[$cidx][1]++;
812                    } else {
813                        $cidx++;
814                        $clusters[$cidx][0] = $i;
815                        $clusters[$cidx][1] = 1;
816                    }
817                } else {
818                    // Create a "cluster" of one since we are just crossing
819                    // a quadrant
820                    $cidx++;
821                    $clusters[$cidx][0] = $i;
822                    $clusters[$cidx][1] = 1;
823                }
824            } else {
825                if ($incluster) {
826                    // Add the last slice
827                    $clusters[$cidx][1]++;
828                    $incluster = false;
829                } else {
830                    // Create a "cluster" of one
831                    $cidx++;
832                    $clusters[$cidx][0] = $i;
833                    $clusters[$cidx][1] = 1;
834                }
835            }
836        }
837        // Handle the very last slice
838        if ($incluster) {
839            $clusters[$cidx][1]++;
840        } else {
841            // Create a "cluster" of one
842            $cidx++;
843            $clusters[$cidx][0] = $i;
844            $clusters[$cidx][1] = 1;
845        }
846
847        /*
848        if( true ) {
849        // Debug printout in labels
850        for( $i=0; $i <= $cidx; ++$i ) {
851        for( $j=0; $j < $clusters[$i][1]; ++$j ) {
852        $a = $this->la[$clusters[$i][0]+$j];
853        $aa = round($a*180/M_PI);
854        $q = $this->Quadrant($a);
855        $this->labels[$clusters[$i][0]+$j]="[$q:$aa] $i:$j";
856        }
857        }
858        }
859         */
860
861        //-----------------------------------------------------------------------
862        // Step 2 of the algorithm is use the clusters and draw the labels
863        // and guidelines
864        //-----------------------------------------------------------------------
865
866        // We use the font height as the base factor for how far we need to
867        // spread the labels in the Y-direction.
868        $this->value->ApplyFont($img);
869        $fh        = $img->GetFontHeight();
870        $origvstep = $fh * $this->iGuideVFactor;
871        $this->value->SetMargin(0);
872
873        // Number of clusters found
874        $nc = count($clusters);
875
876        // Walk through all the clusters
877        for ($i = 0; $i < $nc; ++$i) {
878
879            // Start angle and number of slices in this cluster
880            $csize = $clusters[$i][1];
881            $a     = $this->la[$clusters[$i][0]];
882            $q     = $this->Quadrant($a);
883
884            // Now set up the start and end conditions to make sure that
885            // in each cluster we walk through the all the slices starting with the slice
886            // closest to the equator. Since all slices are numbered clockwise from "3'a clock"
887            // we have different conditions depending on in which quadrant the slice lies within.
888            if ($q == 0) {
889                $start = $csize - 1;
890                $idx   = $start;
891                $step  = -1;
892                $vstep = -$origvstep;
893            } elseif ($q == 1) {
894                $start = 0;
895                $idx   = $start;
896                $step  = 1;
897                $vstep = -$origvstep;
898            } elseif ($q == 2) {
899                $start = $csize - 1;
900                $idx   = $start;
901                $step  = -1;
902                $vstep = $origvstep;
903            } elseif ($q == 3) {
904                $start = 0;
905                $idx   = $start;
906                $step  = 1;
907                $vstep = $origvstep;
908            }
909
910            // Walk through all slices within this cluster
911            for ($j = 0; $j < $csize; ++$j) {
912                // Now adjust the position of the labels in each cluster starting
913                // with the slice that is closest to the equator of the pie
914                $a = $this->la[$clusters[$i][0] + $idx];
915
916                // Guide line start in the center of the arc of the slice
917                $r = $radius + $this->explode_radius[$n - 1 - ($clusters[$i][0] + $idx)];
918                $x = round($r * cos($a) + $xc);
919                $y = round($yc - $r * sin($a));
920
921                // The distance from the arc depends on chosen font and the "R-Factor"
922                $r += $fh * $this->iGuideLineRFactor;
923
924                // Should the labels be placed curved along the pie or in straight columns
925                // outside the pie?
926                if ($this->iGuideLineCurve) {
927                    $xt = round($r * cos($a) + $xc);
928                }
929
930                // If this is the first slice in the cluster we need some first time
931                // proessing
932                if ($idx == $start) {
933                    if (!$this->iGuideLineCurve) {
934                        $xt = round($r * cos($a) + $xc);
935                    }
936
937                    $yt = round($yc - $r * sin($a));
938
939                    // Some special consideration in case this cluster starts
940                    // in quadrant 1 or 3 very close to the "equator" (< 20 degrees)
941                    // and the previous clusters last slice is within the tolerance.
942                    // In that case we add a font height to this labels Y-position
943                    // so it doesn't collide with
944                    // the slice in the previous cluster
945                    $prevcluster = ($i + ($nc - 1)) % $nc;
946                    $previdx     = $clusters[$prevcluster][0] + $clusters[$prevcluster][1] - 1;
947                    if ($q == 1 && $a > 160 * M_PI / 180) {
948                        // Get the angle for the previous clusters last slice
949                        $diff = abs($a - $this->la[$previdx]);
950                        if ($diff < $tresh_hold) {
951                            $yt -= $fh;
952                        }
953                    } elseif ($q == 3 && $a > 340 * M_PI / 180) {
954                        // We need to subtract 360 to compare angle distance between
955                        // q=0 and q=3
956                        $diff = abs($a - $this->la[$previdx] - 360 * M_PI / 180);
957                        if ($diff < $tresh_hold) {
958                            $yt += $fh;
959                        }
960                    }
961                } else {
962                    // The step is at minimum $vstep but if the slices are relatively large
963                    // we make sure that we add at least a step that corresponds to the vertical
964                    // distance between the centers at the arc on the slice
965                    $prev_a = $this->la[$clusters[$i][0] + ($idx - $step)];
966                    $dy     = abs($radius * (sin($a) - sin($prev_a)) * 1.2);
967                    if ($vstep > 0) {
968                        $yt += max($vstep, $dy);
969                    } else {
970                        $yt += min($vstep, -$dy);
971                    }
972                }
973
974                $label = $this->labels[$clusters[$i][0] + $idx];
975
976                if ($csize == 1) {
977                    // A "meta" cluster with only one slice
978                    $r  = $radius + $this->explode_radius[$n - 1 - ($clusters[$i][0] + $idx)];
979                    $rr = $r + $img->GetFontHeight() / 2;
980                    $xt = round($rr * cos($a) + $xc);
981                    $yt = round($yc - $rr * sin($a));
982                    $this->StrokeLabel($label, $img, $xc, $yc, $a, $r);
983                    if ($this->iShowGuideLineForSingle) {
984                        $this->guideline->Stroke($img, $x, $y, $xt, $yt);
985                    }
986                } else {
987                    $this->guideline->Stroke($img, $x, $y, $xt, $yt);
988                    if ($q == 1 || $q == 2) {
989                        // Left side of Pie
990                        $this->guideline->Stroke($img, $xt, $yt, $xt - $this->guidelinemargin, $yt);
991                        $lbladj              = -$this->guidelinemargin - 5;
992                        $this->value->halign = "right";
993                        $this->value->valign = "center";
994                    } else {
995                        // Right side of pie
996                        $this->guideline->Stroke($img, $xt, $yt, $xt + $this->guidelinemargin, $yt);
997                        $lbladj              = $this->guidelinemargin + 5;
998                        $this->value->halign = "left";
999                        $this->value->valign = "center";
1000                    }
1001                    $this->value->Stroke($img, $label, $xt + $lbladj, $yt);
1002                }
1003
1004                // Udate idx to point to next slice in the cluster to process
1005                $idx += $step;
1006            }
1007        }
1008    }
1009
1010    public function StrokeAllLabels($img, $xc, $yc, $radius)
1011    {
1012        // First normalize all angles for labels
1013        $n = count($this->la);
1014        for ($i = 0; $i < $n; ++$i) {
1015            $this->la[$i] = $this->NormAngle($this->la[$i]);
1016        }
1017        if ($this->guideline->iShow) {
1018            $this->StrokeGuideLabels($img, $xc, $yc, $radius);
1019        } else {
1020            $n = count($this->labels);
1021            for ($i = 0; $i < $n; ++$i) {
1022                $this->StrokeLabel($this->labels[$i], $img, $xc, $yc,
1023                    $this->la[$i],
1024                    $radius + $this->explode_radius[$n - 1 - $i]);
1025            }
1026        }
1027    }
1028
1029    // Position the labels of each slice
1030    public function StrokeLabel($label, $img, $xc, $yc, $a, $r)
1031    {
1032
1033        // Default value
1034        if ($this->ilabelposadj === 'auto') {
1035            $this->ilabelposadj = 0.65;
1036        }
1037
1038        // We position the values diferently depending on if they are inside
1039        // or outside the pie
1040        if ($this->ilabelposadj < 1.0) {
1041            $this->value->SetAlign('center', 'center');
1042            $this->value->margin = 0;
1043
1044            $xt = round($this->ilabelposadj * $r * cos($a) + $xc);
1045            $yt = round($yc - $this->ilabelposadj * $r * sin($a));
1046
1047            $this->value->Stroke($img, $label, $xt, $yt);
1048        } else {
1049            $this->value->halign = "left";
1050            $this->value->valign = "top";
1051            $this->value->margin = 0;
1052
1053            // Position the axis title.
1054            // dx, dy is the offset from the top left corner of the bounding box that sorrounds the text
1055            // that intersects with the extension of the corresponding axis. The code looks a little
1056            // bit messy but this is really the only way of having a reasonable position of the
1057            // axis titles.
1058            $this->value->ApplyFont($img);
1059            $h = $img->GetTextHeight($label);
1060            // For numeric values the format of the display value
1061            // must be taken into account
1062            if (is_numeric($label)) {
1063                if ($label > 0) {
1064                    $w = $img->GetTextWidth(sprintf($this->value->format, $label));
1065                } else {
1066                    $w = $img->GetTextWidth(sprintf($this->value->negformat, $label));
1067                }
1068            } else {
1069                $w = $img->GetTextWidth($label);
1070            }
1071
1072            if ($this->ilabelposadj > 1.0 && $this->ilabelposadj < 5.0) {
1073                $r *= $this->ilabelposadj;
1074            }
1075
1076            $r += $img->GetFontHeight() / 1.5;
1077
1078            $xt = round($r * cos($a) + $xc);
1079            $yt = round($yc - $r * sin($a));
1080
1081            // Normalize angle
1082            while ($a < 0) {
1083                $a += 2 * M_PI;
1084            }
1085
1086            while ($a > 2 * M_PI) {
1087                $a -= 2 * M_PI;
1088            }
1089
1090            if ($a >= 7 * M_PI / 4 || $a <= M_PI / 4) {
1091                $dx = 0;
1092            }
1093
1094            if ($a >= M_PI / 4 && $a <= 3 * M_PI / 4) {
1095                $dx = ($a - M_PI / 4) * 2 / M_PI;
1096            }
1097
1098            if ($a >= 3 * M_PI / 4 && $a <= 5 * M_PI / 4) {
1099                $dx = 1;
1100            }
1101
1102            if ($a >= 5 * M_PI / 4 && $a <= 7 * M_PI / 4) {
1103                $dx = (1 - ($a - M_PI * 5 / 4) * 2 / M_PI);
1104            }
1105
1106            if ($a >= 7 * M_PI / 4) {
1107                $dy = (($a - M_PI) - 3 * M_PI / 4) * 2 / M_PI;
1108            }
1109
1110            if ($a <= M_PI / 4) {
1111                $dy = (1 - $a * 2 / M_PI);
1112            }
1113
1114            if ($a >= M_PI / 4 && $a <= 3 * M_PI / 4) {
1115                $dy = 1;
1116            }
1117
1118            if ($a >= 3 * M_PI / 4 && $a <= 5 * M_PI / 4) {
1119                $dy = (1 - ($a - 3 * M_PI / 4) * 2 / M_PI);
1120            }
1121
1122            if ($a >= 5 * M_PI / 4 && $a <= 7 * M_PI / 4) {
1123                $dy = 0;
1124            }
1125
1126            $this->value->Stroke($img, $label, $xt - $dx * $w, $yt - $dy * $h);
1127        }
1128    }
1129
1130    public function UsePlotThemeColors($flag = true)
1131    {
1132        $this->use_plot_theme_colors = $flag;
1133    }
1134} // Class
1135
1136/* EOF */
1137