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