HuasoFoundries/jpgraph

View on GitHub
src/plot/PiePlot.php

Summary

Maintainability
F
1 wk
Test Coverage
<?php

/**
 * JPGraph v4.0.3
 */

namespace Amenadiel\JpGraph\Plot;

use Amenadiel\JpGraph\Graph;
use Amenadiel\JpGraph\Text;
use Amenadiel\JpGraph\Util;

/*
 * File:        JPGRAPH_PIE.PHP
 * // Description: Pie plot extension for JpGraph
 * // Created:     2001-02-14
 * // Ver:         $Id: jpgraph_pie.php 1926 2010-01-11 16:33:07Z ljp $
 * //
 * // Copyright (c) Asial Corporation. All rights reserved.
 */
// Defines for PiePlot::SetLabelType()
define('PIE_VALUE_ABS', 1);
define('PIE_VALUE_PER', 0);
define('PIE_VALUE_PERCENTAGE', 0);
define('PIE_VALUE_ADJPERCENTAGE', 2);
define('PIE_VALUE_ADJPER', 2);

/**
 * @class PiePlot
 * // Description: Draws a pie plot
 */
class PiePlot
{
    public $posx                     = 0.5;
    public $posy                     = 0.5;
    public $is_using_plot_theme      = false;
    public $theme                    = 'earth';
    protected $use_plot_theme_colors = false;
    protected $radius                = 0.3;
    protected $explode_radius        = [];
    protected $explode_all           = false;
    protected $explode_r             = 20;
    protected $labels;
    protected $legends;
    protected $csimtargets;
    protected $csimwintargets; // Array of targets for CSIM
    protected $csimareas = ''; // Generated CSIM text
    protected $csimalts; // ALT tags for corresponding target
    protected $data;
    public $title;
    protected $startangle    = 0;
    protected $weight        = 1;
    protected $color         = 'black';
    protected $legend_margin = 6;
    protected $show_labels   = true;
    protected $themearr      = [
        'earth'  => [136, 34, 40, 45, 46, 62, 63, 134, 74, 10, 120, 136, 141, 168, 180, 77, 209, 218, 346, 395, 89, 430],
        'pastel' => [27, 415, 128, 59, 66, 79, 105, 110, 42, 147, 152, 230, 236, 240, 331, 337, 405, 38],
        'water'  => [8, 370, 24, 40, 335, 56, 213, 237, 268, 14, 326, 387, 10, 388],
        'sand'   => [27, 168, 34, 170, 19, 50, 65, 72, 131, 209, 46, 393], ];
    protected $setslicecolors      = [];
    protected $labeltype           = 0; // Default to percentage
    protected $pie_border          = true;
    protected $pie_interior_border = true;
    public $value;
    protected $ishadowcolor         = '';
    protected $ishadowdrop          = 4;
    protected $ilabelposadj         = 1;
    protected $legendcsimtargets    = [];
    protected $legendcsimwintargets = [];
    protected $legendcsimalts       = [];
    protected $adjusted_data        = [];
    public $guideline;
    protected $guidelinemargin         = 10;
    protected $iShowGuideLineForSingle = false;
    protected $iGuideLineCurve         = false;
    protected $iGuideVFactor           = 1.4;
    protected $iGuideLineRFactor       = 0.8;
    protected $la                      = []; // Holds the exact angle for each label

    /**
     * CONSTRUCTOR.
     *
     * @param mixed $data
     */
    public function __construct($data)
    {
        $this->data  = array_reverse($data);
        $this->title = new Text\Text('');
        $this->title->SetFont(FF_DEFAULT, FS_BOLD);
        $this->value = new DisplayValue();
        $this->value->Show();
        $this->value->SetFormat('%.1f%%');
        $this->guideline = new Graph\LineProperty();
    }

    /**
     * PUBLIC METHODS.
     *
     * @param mixed $x
     * @param mixed $y
     */
    public function SetCenter($x, $y = 0.5)
    {
        $this->posx = $x;
        $this->posy = $y;
    }

    // Enable guideline and set drwaing policy
    public function SetGuideLines($aFlg = true, $aCurved = true, $aAlways = false)
    {
        $this->guideline->Show($aFlg);
        $this->iShowGuideLineForSingle = $aAlways;
        $this->iGuideLineCurve         = $aCurved;
    }

    // Adjuste the distance between labels and labels and pie
    public function SetGuideLinesAdjust($aVFactor, $aRFactor = 0.8)
    {
        $this->iGuideVFactor     = $aVFactor;
        $this->iGuideLineRFactor = $aRFactor;
    }

    public function SetColor($aColor)
    {
        $this->color = $aColor;
    }

    public function SetSliceColors($aColors)
    {
        $this->setslicecolors = $aColors;
    }

    public function SetShadow($aColor = 'darkgray', $aDropWidth = 4)
    {
        $this->ishadowcolor = $aColor;
        $this->ishadowdrop  = $aDropWidth;
    }

    public function SetCSIMTargets($aTargets, $aAlts = '', $aWinTargets = '')
    {
        $this->csimtargets = array_reverse($aTargets);
        if (is_array($aWinTargets)) {
            $this->csimwintargets = array_reverse($aWinTargets);
        }

        if (is_array($aAlts)) {
            $this->csimalts = array_reverse($aAlts);
        }
    }

    public function GetCSIMareas()
    {
        return $this->csimareas;
    }

    public function AddSliceToCSIM($i, $xc, $yc, $radius, $sa, $ea)
    {
        //Slice number, ellipse centre (x,y), height, width, start angle, end angle
        while ($sa > 2 * M_PI) {
            $sa = $sa - 2 * M_PI;
        }

        while ($ea > 2 * M_PI) {
            $ea = $ea - 2 * M_PI;
        }

        $sa = 2 * M_PI - $sa;
        $ea = 2 * M_PI - $ea;

        // Special case when we have only one slice since then both start and end
        // angle will be == 0
        if (abs($sa - $ea) < 0.0001) {
            $sa = 2 * M_PI;
            $ea = 0;
        }

        //add coordinates of the centre to the map
        $xc     = floor($xc);
        $yc     = floor($yc);
        $coords = "${xc}, ${yc}";

        //add coordinates of the first point on the arc to the map
        $xp = floor(($radius * cos($ea)) + $xc);
        $yp = floor($yc - $radius * sin($ea));
        $coords .= ", ${xp}, ${yp}";

        //add coordinates every 0.2 radians
        $a = $ea + 0.2;

        // If we cross the 360-limit with a slice we need to handle
        // the fact that end angle is smaller than start
        if ($sa < $ea) {
            while ($a <= 2 * M_PI) {
                $xp = floor($radius * cos($a) + $xc);
                $yp = floor($yc - $radius * sin($a));
                $coords .= ", ${xp}, ${yp}";
                $a += 0.2;
            }
            $a -= 2 * M_PI;
        }

        while ($a < $sa) {
            $xp = floor($radius * cos($a) + $xc);
            $yp = floor($yc - $radius * sin($a));
            $coords .= ", ${xp}, ${yp}";
            $a += 0.2;
        }

        //Add the last point on the arc
        $xp = floor($radius * cos($sa) + $xc);
        $yp = floor($yc - $radius * sin($sa));
        $coords .= ", ${xp}, ${yp}";
        if (!empty($this->csimtargets[$i])) {
            $this->csimareas .= "<area shape=\"poly\" coords=\"${coords}\" href=\"" . $this->csimtargets[$i] . '"';
            $tmp = '';
            if (!empty($this->csimwintargets[$i])) {
                $this->csimareas .= ' target="' . $this->csimwintargets[$i] . '" ';
            }
            if (!empty($this->csimalts[$i])) {
                $tmp = sprintf($this->csimalts[$i], $this->data[$i]);
                $this->csimareas .= " title=\"${tmp}\" alt=\"${tmp}\" ";
            }
            $this->csimareas .= " />\n";
        }
    }

    public function SetTheme($aTheme)
    {
        //        Util\JpGraphError::RaiseL(15012,$aTheme);
        //        return;

        if (in_array($aTheme, array_keys($this->themearr), true)) {
            $this->theme               = $aTheme;
            $this->is_using_plot_theme = true;
        } else {
            Util\JpGraphError::RaiseL(15001, $aTheme); //("PiePLot::SetTheme() Unknown theme: $aTheme");
        }
    }

    public function ExplodeSlice($e, $radius = 20)
    {
        if (!is_integer($e)) {
            Util\JpGraphError::RaiseL(15002);
        }
        //('Argument to PiePlot::ExplodeSlice() must be an integer');
        $this->explode_radius[$e] = $radius;
    }

    public function ExplodeAll($radius = 20)
    {
        $this->explode_all = true;
        $this->explode_r   = $radius;
    }

    public function Explode($aExplodeArr)
    {
        if (!is_array($aExplodeArr)) {
            Util\JpGraphError::RaiseL(15003);
            //("Argument to PiePlot::Explode() must be an array with integer distances.");
        }
        $this->explode_radius = $aExplodeArr;
    }

    public function SetStartAngle($aStart)
    {
        if ($aStart < 0 || $aStart > 360) {
            Util\JpGraphError::RaiseL(15004); //('Slice start angle must be between 0 and 360 degrees.');
        }
        if ($aStart == 0) {
            $this->startangle = 0;
        } else {
            $this->startangle = 360 - $aStart;
            $this->startangle *= M_PI / 180;
        }
    }

    // Size in percentage
    public function SetSize($aSize)
    {
        if (($aSize > 0 && $aSize <= 0.5) || ($aSize > 10 && $aSize < 1000)) {
            $this->radius = $aSize;
        } else {
            Util\JpGraphError::RaiseL(15006);
        }

        //("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]");
    }

    // Set label arrays
    public function SetLegends($aLegend)
    {
        $this->legends = $aLegend;
    }

    // Set text labels for slices
    public function SetLabels($aLabels, $aLblPosAdj = 'auto')
    {
        $this->labels       = array_reverse($aLabels);
        $this->ilabelposadj = $aLblPosAdj;
    }

    public function SetLabelPos($aLblPosAdj)
    {
        $this->ilabelposadj = $aLblPosAdj;
    }

    // Should we display actual value or percentage?
    public function SetLabelType($aType)
    {
        if ($aType < 0 || $aType > 2) {
            Util\JpGraphError::RaiseL(15008, $aType);
        }

        //("PiePlot::SetLabelType() Type for pie plots must be 0 or 1 (not $t).");
        $this->labeltype = $aType;
    }

    // Deprecated.
    public function SetValueType($aType)
    {
        $this->SetLabelType($aType);
    }

    // Should the circle around a pie plot be displayed
    public function ShowBorder($exterior = true, $interior = true)
    {
        $this->pie_border          = $exterior;
        $this->pie_interior_border = $interior;
    }

    // Setup the legends
    public function Legend($graph)
    {
        $colors = array_keys($graph->img->rgb->rgb_table);
        sort($colors);
        $ta = $this->themearr[$this->theme];
        $n  = safe_count($this->data);

        if ($this->setslicecolors == null) {
            $numcolors = safe_count($ta);
            if (($this instanceof PiePlot3D)) {
                $ta = array_reverse(array_slice($ta, 0, $n));
            }
        } else {
            $this->setslicecolors = array_slice($this->setslicecolors, 0, $n);
            $numcolors            = safe_count($this->setslicecolors);
            if ($graph->pieaa && !($this instanceof PiePlot3D)) {
                $this->setslicecolors = array_reverse($this->setslicecolors);
            }
        }

        $sum = 0;
        for ($i = 0; $i < $n; ++$i) {
            $sum += $this->data[$i];
        }

        // Bail out with error if the sum is 0
        if ($sum == 0) {
            Util\JpGraphError::RaiseL(15009);
        }
        //("Illegal pie plot. Sum of all data is zero for Pie!");

        // Make sure we don't plot more values than data points
        // (in case the user added more legends than data points)
        $n = min(safe_count($this->legends), safe_count($this->data));
        if ($this->legends != '') {
            $this->legends = array_reverse(array_slice($this->legends, 0, $n));
        }
        for ($i = $n - 1; $i >= 0; --$i) {
            $l = $this->legends[$i];
            // Replace possible format with actual values
            if (safe_count($this->csimalts) > $i) {
                $fmt = $this->csimalts[$i];
            } else {
                $fmt = '%d'; // Deafult Alt if no other has been specified
            }
            if ($this->labeltype == 0) {
                $l   = sprintf($l, 100 * $this->data[$i] / $sum);
                $alt = sprintf($fmt, $this->data[$i]);
            } elseif ($this->labeltype == 1) {
                $l   = sprintf($l, $this->data[$i]);
                $alt = sprintf($fmt, $this->data[$i]);
            } else {
                $l   = sprintf($l, $this->adjusted_data[$i]);
                $alt = sprintf($fmt, $this->adjusted_data[$i]);
            }

            if (empty($this->csimwintargets[$i])) {
                $wintarg = '';
            } else {
                $wintarg = $this->csimwintargets[$i];
            }

            $imageMapTarget = $this->csimtargets[$i] ?? '';
            if ($this->setslicecolors == null) {
                $graph->legend->Add($l, $colors[$ta[$i % $numcolors]], '', 0, $imageMapTarget, $alt, $wintarg);
            } else {
                $graph->legend->Add($l, $this->setslicecolors[$i % $numcolors], '', 0, $imageMapTarget, $alt, $wintarg);
            }
        }
    }

    // Adjust the rounded percetage value so that the sum of
    // of the pie slices are always 100%
    // Using the Hare/Niemeyer method
    public function AdjPercentage($aData, $aPrec = 0)
    {
        $mul = 100;
        if ($aPrec > 0 && $aPrec < 3) {
            if ($aPrec == 1) {
                $mul = 1000;
            } else {
                $mul = 10000;
            }
        }

        $tmp       = [];
        $result    = [];
        $quote_sum = 0;
        $n         = safe_count($aData);
        for ($i = 0, $sum = 0; $i < $n; ++$i) {
            $sum += $aData[$i];
        }

        foreach ($aData as $index => $value) {
            $tmp_percentage = $value / $sum * $mul;
            $result[$index] = floor($tmp_percentage);
            $tmp[$index]    = $tmp_percentage - $result[$index];
            $quote_sum += $result[$index];
        }
        if ($quote_sum == $mul) {
            if ($mul > 100) {
                $tmp = $mul / 100;
                for ($i = 0; $i < $n; ++$i) {
                    $result[$i] /= $tmp;
                }
            }

            return $result;
        }
        arsort($tmp, SORT_NUMERIC);
        reset($tmp);
        for ($i = 0; $i < $mul - $quote_sum; ++$i) {
            ++$result[key($tmp)];
            next($tmp);
        }
        if ($mul > 100) {
            $tmp = $mul / 100;
            for ($i = 0; $i < $n; ++$i) {
                $result[$i] /= $tmp;
            }
        }

        return $result;
    }

    public function Stroke($img, $aaoption = 0)
    {
        // aaoption is used to handle antialias
        // aaoption == 0 a normal pie
        // aaoption == 1 just the body
        // aaoption == 2 just the values

        // Explode scaling. If anti alias we scale the image
        // twice and we also need to scale the exploding distance
        $expscale = $aaoption === 1 ? 2 : 1;

        if ($this->labeltype == 2) {
            // Adjust the data so that it will add up to 100%
            $this->adjusted_data = $this->AdjPercentage($this->data);
        }

        if ($this->use_plot_theme_colors) {
            $this->setslicecolors = null;
        }

        $colors = array_keys($img->rgb->rgb_table);
        sort($colors);
        $ta = $this->themearr[$this->theme];
        $n  = safe_count($this->data);

        if ($this->setslicecolors == null) {
            $numcolors = safe_count($ta);
        } else {
            // We need to create an array of colors as long as the data
            // since we need to reverse it to get the colors in the right order
            $numcolors = safe_count($this->setslicecolors);
            $i         = 2 * $numcolors;
            while ($n > $i) {
                $this->setslicecolors = array_merge($this->setslicecolors, $this->setslicecolors);
                $i += $n;
            }
            $tt                   = array_slice($this->setslicecolors, 0, $n % $numcolors);
            $this->setslicecolors = array_merge($this->setslicecolors, $tt);
            $this->setslicecolors = array_reverse($this->setslicecolors);
        }

        // Draw the slices
        $sum = 0;
        for ($i = 0; $i < $n; ++$i) {
            $sum += $this->data[$i];
        }

        // Bail out with error if the sum is 0
        if ($sum == 0) {
            Util\JpGraphError::RaiseL(15009); //("Sum of all data is 0 for Pie.");
        }

        // Set up the pie-circle
        if ($this->radius <= 1) {
            $radius = floor($this->radius * min($img->width, $img->height));
        } else {
            $radius = $aaoption === 1 ? $this->radius * 2 : $this->radius;
        }

        if ($this->posx <= 1 && $this->posx > 0) {
            $xc = round($this->posx * $img->width);
        } else {
            $xc = $this->posx;
        }

        if ($this->posy <= 1 && $this->posy > 0) {
            $yc = round($this->posy * $img->height);
        } else {
            $yc = $this->posy;
        }

        $n = safe_count($this->data);

        if ($this->explode_all) {
            for ($i = 0; $i < $n; ++$i) {
                $this->explode_radius[$i] = $this->explode_r;
            }
        }

        // If we have a shadow and not just drawing the labels
        if ($this->ishadowcolor != '' && $aaoption !== 2) {
            $accsum = 0;
            $angle2 = $this->startangle;
            $img->SetColor($this->ishadowcolor);
            for ($i = 0; $sum > 0 && $i < $n; ++$i) {
                $j      = $n - $i - 1;
                $d      = $this->data[$i];
                $angle1 = $angle2;
                $accsum += $d;
                $angle2 = $this->startangle + 2 * M_PI * $accsum / $sum;
                if (empty($this->explode_radius[$j])) {
                    $this->explode_radius[$j] = 0;
                }

                if ($d < 0.00001) {
                    continue;
                }

                $la = 2 * M_PI - (abs($angle2 - $angle1) / 2.0 + $angle1);

                $xcm = $xc + $this->explode_radius[$j] * cos($la) * $expscale;
                $ycm = $yc - $this->explode_radius[$j] * sin($la) * $expscale;

                $xcm += $this->ishadowdrop * $expscale;
                $ycm += $this->ishadowdrop * $expscale;

                $_sa = round($angle1 * 180 / M_PI);
                $_ea = round($angle2 * 180 / M_PI);

                // The CakeSlice method draws a full circle in case of start angle = end angle
                // for pie slices we don't want this behaviour unless we only have one
                // slice in the pie in case it is the wanted behaviour
                if ($_ea - $_sa > 0.1 || $n == 1) {
                    $img->CakeSlice(
                        $xcm,
                        $ycm,
                        $radius - 1,
                        $radius - 1,
                        $angle1 * 180 / M_PI,
                        $angle2 * 180 / M_PI,
                        $this->ishadowcolor
                    );
                }
            }
        }

        /**
         * This is the main loop to draw each cake slice.
         */
        // Set up the accumulated sum, start angle for first slice and border color
        $accsum = 0;
        $angle2 = $this->startangle;
        $img->SetColor($this->color);

        // Loop though all the slices if there is a pie to draw (sum>0)
        // There are n slices in total
        for ($i = 0; $sum > 0 && $i < $n; ++$i) {
            // $j is the actual index used for the slice
            $j = $n - $i - 1;

            // Make sure we havea  valid distance to explode the slice
            if (empty($this->explode_radius[$j])) {
                $this->explode_radius[$j] = 0;
            }

            // The actual numeric value for the slice
            $d = $this->data[$i];

            $angle1 = $angle2;

            // Accumlate the sum
            $accsum += $d;

            // The new angle when we add the "size" of this slice
            // angle1 is then the start and angle2 the end of this slice
            $angle2 = $this->NormAngle($this->startangle + 2 * M_PI * $accsum / $sum);

            // We avoid some trouble by not allowing end angle to be 0, in that case
            // we translate to 360

            // la is used to hold the label angle, which is centered on the slice
            if ($angle2 < 0.0001 && $angle1 > 0.0001) {
                $this->la[$i] = 2 * M_PI - (abs(2 * M_PI - $angle1) / 2.0 + $angle1);
            } elseif ($angle1 > $angle2) {
                // The case where the slice crosses the 3 a'clock line
                // Remember that the slices are counted clockwise and
                // labels are counted counter clockwise so we need to revert with 2 PI
                $this->la[$i] = 2 * M_PI - $this->NormAngle($angle1 + ((2 * M_PI - $angle1) + $angle2) / 2);
            } else {
                $this->la[$i] = 2 * M_PI - (abs($angle2 - $angle1) / 2.0 + $angle1);
            }

            // Too avoid rounding problems we skip the slice if it is too small
            if ($d < 0.00001) {
                continue;
            }

            // If the user has specified an array of colors for each slice then use
            // that a color otherwise use the theme array (ta) of colors
            if ($this->setslicecolors == null) {
                $slicecolor = $colors[$ta[$i % $numcolors]];
            } else {
                $slicecolor = $this->setslicecolors[$i % $numcolors];
            }

            //            $_sa = round($angle1*180/M_PI);
            //            $_ea = round($angle2*180/M_PI);
            //            $_la = round($this->la[$i]*180/M_PI);
            //            echo "Slice#$i: ang1=$_sa , ang2=$_ea, la=$_la, color=$slicecolor<br>";

            // If we have enabled antialias then we don't draw any border so
            // make the bordedr color the same as the slice color
            if ($this->pie_interior_border && $aaoption === 0) {
                $img->SetColor($this->color);
            } else {
                $img->SetColor($slicecolor);
            }
            $arccolor = $this->pie_border && $aaoption === 0 ? $this->color : '';

            // Calculate the x,y coordinates for the base of this slice taking
            // the exploded distance into account. Here we use the mid angle as the
            // ray of extension and we have the mid angle handy as it is also the
            // label angle
            $xcm = $xc + $this->explode_radius[$j] * cos($this->la[$i]) * $expscale;
            $ycm = $yc - $this->explode_radius[$j] * sin($this->la[$i]) * $expscale;

            // If we are not just drawing the labels then draw this cake slice
            if ($aaoption !== 2) {
                $_sa = round($angle1 * 180 / M_PI);
                $_ea = round($angle2 * 180 / M_PI);
                $_la = round($this->la[$i] * 180 / M_PI);
                //echo "[$i] sa=$_sa, ea=$_ea, la[$i]=$_la, (color=$slicecolor)<br>";

                // The CakeSlice method draws a full circle in case of start angle = end angle
                // for pie slices we want this in case the slice have a value larger than 99% of the
                // total sum
                if (abs($_ea - $_sa) >= 1 || $d == $sum) {
                    $img->CakeSlice($xcm, $ycm, $radius - 1, $radius - 1, $_sa, $_ea, $slicecolor, $arccolor);
                }
            }

            // If the CSIM is used then make sure we register a CSIM area for this slice as well
            if ($this->csimtargets && $aaoption !== 1) {
                $this->AddSliceToCSIM($i, $xcm, $ycm, $radius, $angle1, $angle2);
            }
        }

        // Format the titles for each slice
        if ($aaoption !== 2) {
            for ($i = 0; $i < $n; ++$i) {
                if ($this->labeltype == 0) {
                    if ($sum != 0) {
                        $l = 100.0 * $this->data[$i] / $sum;
                    } else {
                        $l = 0.0;
                    }
                } elseif ($this->labeltype == 1) {
                    $l = $this->data[$i] * 1.0;
                } else {
                    $l = $this->adjusted_data[$i];
                }
                if (isset($this->labels[$i]) && is_string($this->labels[$i])) {
                    $this->labels[$i] = sprintf($this->labels[$i], $l);
                } else {
                    $this->labels[$i] = $l;
                }
            }
        }

        if ($this->value->show && $aaoption !== 1) {
            $this->StrokeAllLabels($img, $xc, $yc, $radius);
        }

        // Adjust title position
        if ($aaoption !== 1) {
            $this->title->SetPos(
                $xc,
                $yc - $this->title->GetFontHeight($img) - $radius - $this->title->margin,
                'center',
                'bottom'
            );
            $this->title->Stroke($img);
        }
    }

    /**
     * PRIVATE METHODS.
     *
     * @param mixed $a
     */
    public function NormAngle($a)
    {
        while ($a < 0) {
            $a += 2 * M_PI;
        }

        while ($a > 2 * M_PI) {
            $a -= 2 * M_PI;
        }

        return $a;
    }

    public function Quadrant($a)
    {
        $a = $this->NormAngle($a);
        if ($a > 0 && $a <= M_PI / 2) {
            return 0;
        }

        if ($a > M_PI / 2 && $a <= M_PI) {
            return 1;
        }

        if ($a > M_PI && $a <= 1.5 * M_PI) {
            return 2;
        }

        if ($a > 1.5 * M_PI) {
            return 3;
        }
    }

    public function StrokeGuideLabels($img, $xc, $yc, $radius)
    {
        $n = safe_count($this->labels);

        /**
         * Step 1 of the algorithm is to construct a number of clusters
         * // a cluster is defined as all slices within the same quadrant (almost)
         * // that has an angular distance less than the treshold.
         */
        $tresh_hold = 25 * M_PI / 180; // 25 degrees difference to be in a cluster
        $incluster  = false; // flag if we are currently in a cluster or not
        $clusters   = []; // array of clusters
        $cidx       = -1; // running cluster index

        // Go through all the labels and construct a number of clusters
        for ($i = 0; $i < $n - 1; ++$i) {
            // Calc the angle distance between two consecutive slices
            $a1   = $this->la[$i];
            $a2   = $this->la[$i + 1];
            $q1   = $this->Quadrant($a1);
            $q2   = $this->Quadrant($a2);
            $diff = abs($a1 - $a2);
            if ($diff < $tresh_hold) {
                if ($incluster) {
                    ++$clusters[$cidx][1];
                    // Each cluster can only cover one quadrant
                    // Do we cross a quadrant ( and must break the cluster)
                    if ($q1 != $q2) {
                        // If we cross a quadrant boundary we normally start a
                        // new cluster. However we need to take the 12'a clock
                        // and 6'a clock positions into a special consideration.
                        // Case 1: WE go from q=1 to q=2 if the last slice on
                        // the cluster for q=1 is close to 12'a clock and the
                        // first slice in q=0 is small we extend the previous
                        // cluster
                        if ($q1 == 1 && $q2 == 0 && $a2 > (90 - 15) * M_PI / 180) {
                            if ($i < $n - 2) {
                                $a3 = $this->la[$i + 2];
                                // If there isn't a cluster coming up with the next-next slice
                                // we extend the previous cluster to cover this slice as well
                                if (abs($a3 - $a2) >= $tresh_hold) {
                                    ++$clusters[$cidx][1];
                                    ++$i;
                                }
                            }
                        } elseif ($q1 == 3 && $q2 == 2 && $a2 > (270 - 15) * M_PI / 180) {
                            if ($i < $n - 2) {
                                $a3 = $this->la[$i + 2];
                                // If there isn't a cluster coming up with the next-next slice
                                // we extend the previous cluster to cover this slice as well
                                if (abs($a3 - $a2) >= $tresh_hold) {
                                    ++$clusters[$cidx][1];
                                    ++$i;
                                }
                            }
                        }

                        if ($q1 == 2 && $q2 == 1 && $a2 > (180 - 15) * M_PI / 180) {
                            ++$clusters[$cidx][1];
                            ++$i;
                        }

                        $incluster = false;
                    }
                } elseif ($q1 == $q2) {
                    $incluster = true;
                    // Now we have a special case for quadrant 0. If we previously
                    // have a cluster of one in quadrant 0 we just extend that
                    // cluster. If we don't do this then we risk that the label
                    // for the cluster of one will cross the guide-line
                    if ($q1 == 0 && $cidx > -1 &&
                        $clusters[$cidx][1] == 1 &&
                        $this->Quadrant($this->la[$clusters[$cidx][0]]) == 0) {
                        ++$clusters[$cidx][1];
                    } else {
                        ++$cidx;
                        $clusters[$cidx][0] = $i;
                        $clusters[$cidx][1] = 1;
                    }
                } else {
                    // Create a "cluster" of one since we are just crossing
                    // a quadrant
                    ++$cidx;
                    $clusters[$cidx][0] = $i;
                    $clusters[$cidx][1] = 1;
                }
            } else {
                if ($incluster) {
                    // Add the last slice
                    ++$clusters[$cidx][1];
                    $incluster = false;
                } else {
                    // Create a "cluster" of one
                    ++$cidx;
                    $clusters[$cidx][0] = $i;
                    $clusters[$cidx][1] = 1;
                }
            }
        }
        // Handle the very last slice
        if ($incluster) {
            ++$clusters[$cidx][1];
        } else {
            // Create a "cluster" of one
            ++$cidx;
            $clusters[$cidx][0] = $i;
            $clusters[$cidx][1] = 1;
        }

        /*
        if( true ) {
        // Debug printout in labels
        for( $i=0; $i <= $cidx; ++$i ) {
        for( $j=0; $j < $clusters[$i][1]; ++$j ) {
        $a = $this->la[$clusters[$i][0]+$j];
        $aa = round($a*180/M_PI);
        $q = $this->Quadrant($a);
        $this->labels[$clusters[$i][0]+$j]="[$q:$aa] $i:$j";
        }
        }
        }
         */

        /*
         * Step 2 of the algorithm is use the clusters and draw the labels
         * // and guidelines
         */
        // We use the font height as the base factor for how far we need to
        // spread the labels in the Y-direction.
        $this->value->ApplyFont($img);
        $fh        = $img->GetFontHeight();
        $origvstep = $fh * $this->iGuideVFactor;
        $this->value->SetMargin(0);

        // Number of clusters found
        $nc = safe_count($clusters);

        // Walk through all the clusters
        for ($i = 0; $i < $nc; ++$i) {
            // Start angle and number of slices in this cluster
            $csize = $clusters[$i][1];
            $a     = $this->la[$clusters[$i][0]];
            $q     = $this->Quadrant($a);

            // Now set up the start and end conditions to make sure that
            // in each cluster we walk through the all the slices starting with the slice
            // closest to the equator. Since all slices are numbered clockwise from "3'a clock"
            // we have different conditions depending on in which quadrant the slice lies within.
            if ($q == 0) {
                $start = $csize - 1;
                $idx   = $start;
                $step  = -1;
                $vstep = -$origvstep;
            } elseif ($q == 1) {
                $start = 0;
                $idx   = $start;
                $step  = 1;
                $vstep = -$origvstep;
            } elseif ($q == 2) {
                $start = $csize - 1;
                $idx   = $start;
                $step  = -1;
                $vstep = $origvstep;
            } elseif ($q == 3) {
                $start = 0;
                $idx   = $start;
                $step  = 1;
                $vstep = $origvstep;
            }

            // Walk through all slices within this cluster
            for ($j = 0; $j < $csize; ++$j) {
                // Now adjust the position of the labels in each cluster starting
                // with the slice that is closest to the equator of the pie
                $a = $this->la[$clusters[$i][0] + $idx];

                // Guide line start in the center of the arc of the slice
                $r = $radius + $this->explode_radius[$n - 1 - ($clusters[$i][0] + $idx)];
                $x = round($r * cos($a) + $xc);
                $y = round($yc - $r * sin($a));

                // The distance from the arc depends on chosen font and the "R-Factor"
                $r += $fh * $this->iGuideLineRFactor;

                // Should the labels be placed curved along the pie or in straight columns
                // outside the pie?
                if ($this->iGuideLineCurve) {
                    $xt = round($r * cos($a) + $xc);
                }

                // If this is the first slice in the cluster we need some first time
                // proessing
                if ($idx == $start) {
                    if (!$this->iGuideLineCurve) {
                        $xt = round($r * cos($a) + $xc);
                    }

                    $yt = round($yc - $r * sin($a));

                    // Some special consideration in case this cluster starts
                    // in quadrant 1 or 3 very close to the "equator" (< 20 degrees)
                    // and the previous clusters last slice is within the tolerance.
                    // In that case we add a font height to this labels Y-position
                    // so it doesn't collide with
                    // the slice in the previous cluster
                    $prevcluster = ($i + ($nc - 1)) % $nc;
                    $previdx     = $clusters[$prevcluster][0] + $clusters[$prevcluster][1] - 1;
                    if ($q == 1 && $a > 160 * M_PI / 180) {
                        // Get the angle for the previous clusters last slice
                        $diff = abs($a - $this->la[$previdx]);
                        if ($diff < $tresh_hold) {
                            $yt -= $fh;
                        }
                    } elseif ($q == 3 && $a > 340 * M_PI / 180) {
                        // We need to subtract 360 to compare angle distance between
                        // q=0 and q=3
                        $diff = abs($a - $this->la[$previdx] - 360 * M_PI / 180);
                        if ($diff < $tresh_hold) {
                            $yt += $fh;
                        }
                    }
                } else {
                    // The step is at minimum $vstep but if the slices are relatively large
                    // we make sure that we add at least a step that corresponds to the vertical
                    // distance between the centers at the arc on the slice
                    $prev_a = $this->la[$clusters[$i][0] + ($idx - $step)];
                    $dy     = abs($radius * (sin($a) - sin($prev_a)) * 1.2);
                    if ($vstep > 0) {
                        $yt += max($vstep, $dy);
                    } else {
                        $yt += min($vstep, -$dy);
                    }
                }

                $label = $this->labels[$clusters[$i][0] + $idx];

                if ($csize == 1) {
                    // A "meta" cluster with only one slice
                    $r  = $radius + $this->explode_radius[$n - 1 - ($clusters[$i][0] + $idx)];
                    $rr = $r + $img->GetFontHeight() / 2;
                    $xt = round($rr * cos($a) + $xc);
                    $yt = round($yc - $rr * sin($a));
                    $this->StrokeLabel($label, $img, $xc, $yc, $a, $r);
                    if ($this->iShowGuideLineForSingle) {
                        $this->guideline->Stroke($img, $x, $y, $xt, $yt);
                    }
                } else {
                    $this->guideline->Stroke($img, $x, $y, $xt, $yt);
                    if ($q == 1 || $q == 2) {
                        // Left side of Pie
                        $this->guideline->Stroke($img, $xt, $yt, $xt - $this->guidelinemargin, $yt);
                        $lbladj              = -$this->guidelinemargin - 5;
                        $this->value->halign = 'right';
                        $this->value->valign = 'center';
                    } else {
                        // Right side of pie
                        $this->guideline->Stroke($img, $xt, $yt, $xt + $this->guidelinemargin, $yt);
                        $lbladj              = $this->guidelinemargin + 5;
                        $this->value->halign = 'left';
                        $this->value->valign = 'center';
                    }
                    $this->value->Stroke($img, $label, $xt + $lbladj, $yt);
                }

                // Udate idx to point to next slice in the cluster to process
                $idx += $step;
            }
        }
    }

    public function StrokeAllLabels($img, $xc, $yc, $radius)
    {
        // First normalize all angles for labels
        $n = safe_count($this->la);
        for ($i = 0; $i < $n; ++$i) {
            $this->la[$i] = $this->NormAngle($this->la[$i]);
        }
        if ($this->guideline->iShow) {
            $this->StrokeGuideLabels($img, $xc, $yc, $radius);
        } else {
            $n = safe_count($this->labels);
            for ($i = 0; $i < $n; ++$i) {
                $this->StrokeLabel(
                    $this->labels[$i],
                    $img,
                    $xc,
                    $yc,
                    $this->la[$i],
                    $radius + $this->explode_radius[$n - 1 - $i]
                );
            }
        }
    }

    // Position the labels of each slice
    public function StrokeLabel($label, $img, $xc, $yc, $a, $r)
    {
        // Default value
        if ($this->ilabelposadj === 'auto') {
            $this->ilabelposadj = 0.65;
        }

        // We position the values diferently depending on if they are inside
        // or outside the pie
        if ($this->ilabelposadj < 1.0) {
            $this->value->SetAlign('center', 'center');
            $this->value->margin = 0;

            $xt = round($this->ilabelposadj * $r * cos($a) + $xc);
            $yt = round($yc - $this->ilabelposadj * $r * sin($a));

            $this->value->Stroke($img, $label, $xt, $yt);
        } else {
            $this->value->halign = 'left';
            $this->value->valign = 'top';
            $this->value->margin = 0;

            // Position the axis title.
            // dx, dy is the offset from the top left corner of the bounding box that sorrounds the text
            // that intersects with the extension of the corresponding axis. The code looks a little
            // bit messy but this is really the only way of having a reasonable position of the
            // axis titles.
            $this->value->ApplyFont($img);
            $h = $img->GetTextHeight($label);
            // For numeric values the format of the display value
            // must be taken into account
            if (is_numeric($label)) {
                if ($label > 0) {
                    $w = $img->GetTextWidth(sprintf($this->value->format, $label));
                } else {
                    $w = $img->GetTextWidth(sprintf($this->value->negformat, $label));
                }
            } else {
                $w = $img->GetTextWidth($label);
            }

            if ($this->ilabelposadj > 1.0 && $this->ilabelposadj < 5.0) {
                $r *= $this->ilabelposadj;
            }

            $r += $img->GetFontHeight() / 1.5;

            $xt = round($r * cos($a) + $xc);
            $yt = round($yc - $r * sin($a));

            // Normalize angle
            while ($a < 0) {
                $a += 2 * M_PI;
            }

            while ($a > 2 * M_PI) {
                $a -= 2 * M_PI;
            }

            if ($a >= 7 * M_PI / 4 || $a <= M_PI / 4) {
                $dx = 0;
            }

            if ($a >= M_PI / 4 && $a <= 3 * M_PI / 4) {
                $dx = ($a - M_PI / 4) * 2 / M_PI;
            }

            if ($a >= 3 * M_PI / 4 && $a <= 5 * M_PI / 4) {
                $dx = 1;
            }

            if ($a >= 5 * M_PI / 4 && $a <= 7 * M_PI / 4) {
                $dx = (1 - ($a - M_PI * 5 / 4) * 2 / M_PI);
            }

            if ($a >= 7 * M_PI / 4) {
                $dy = (($a - M_PI) - 3 * M_PI / 4) * 2 / M_PI;
            }

            if ($a <= M_PI / 4) {
                $dy = (1 - $a * 2 / M_PI);
            }

            if ($a >= M_PI / 4 && $a <= 3 * M_PI / 4) {
                $dy = 1;
            }

            if ($a >= 3 * M_PI / 4 && $a <= 5 * M_PI / 4) {
                $dy = (1 - ($a - 3 * M_PI / 4) * 2 / M_PI);
            }

            if ($a >= 5 * M_PI / 4 && $a <= 7 * M_PI / 4) {
                $dy = 0;
            }

            $this->value->Stroke($img, $label, $xt - $dx * $w, $yt - $dy * $h);
        }
    }

    public function UsePlotThemeColors($flag = true)
    {
        $this->use_plot_theme_colors = $flag;
    }
} // @class

/* EOF */