1<?php 2/*======================================================================= 3// File: JPGRAPH_PIE3D.PHP 4// Description: 3D Pie plot extension for JpGraph 5// Created: 2001-03-24 6// Author: Johan Persson (johanp@aditus.nu) 7// Ver: $Id: jpgraph_pie3d.php,v 1.1.1.1 2005/11/30 23:01:57 gth2 Exp $ 8// 9// License: This code is released under QPL 10// Copyright (C) 2001,2002 Johan Persson 11//======================================================================== 12*/ 13 14 15// Debug print 16function dbgp($str) { 17// echo $str; 18} 19 20//=================================================== 21// CLASS PiePlot3D 22// Description: Plots a 3D pie with a specified projection 23// angle between 20 and 70 degrees. 24//=================================================== 25class PiePlot3D extends PiePlot { 26 var $labelhintcolor="red",$showlabelhint=true,$labelmargin=0.30; 27 var $angle=35; 28 var $edgecolor="", $edgeweight=1; 29 var $iThickness=false; 30 31//--------------- 32// CONSTRUCTOR 33 function PiePlot3d(&$data) { 34 $this->radius = 0.5; 35 $this->data = $data; 36 $this->title = new Text(""); 37 $this->title->SetFont(FF_FONT1,FS_BOLD); 38 $this->value = new DisplayValue(); 39 $this->value->Show(); 40 $this->value->SetFormat('%.0f%%'); 41 } 42 43//--------------- 44// PUBLIC METHODS 45 46 // Should the slices be separated by a line? If color is specified as "" no line 47 // will be used to separate pie slices. 48 function SetEdge($aColor,$aWeight=1) { 49 $this->edgecolor = $aColor; 50 $this->edgeweight = $aWeight; 51 } 52 53 // Specify projection angle for 3D in degrees 54 // Must be between 20 and 70 degrees 55 function SetAngle($a) { 56 if( $a<5 || $a>90 ) 57 JpGraphError::Raise("PiePlot3D::SetAngle() 3D Pie projection angle must be between 5 and 85 degrees."); 58 else 59 $this->angle = $a; 60 } 61 62 function AddSliceToCSIM($i,$xc,$yc,$height,$width,$thick,$sa,$ea) { //Slice number, ellipse centre (x,y), height, width, start angle, end angle 63 64 $sa *= M_PI/180; 65 $ea *= M_PI/180; 66 67 //add coordinates of the centre to the map 68 $coords = "$xc, $yc"; 69 70 //add coordinates of the first point on the arc to the map 71 $xp = floor($width*cos($sa)/2+$xc); 72 $yp = floor($yc-$height*sin($sa)/2); 73 $coords.= ", $xp, $yp"; 74 75 //If on the front half, add the thickness offset 76 if ($sa >= M_PI && $sa <= 2*M_PI*1.01) { 77 $yp = floor($yp+$thick); 78 $coords.= ", $xp, $yp"; 79 } 80 81 //add coordinates every 0.2 radians 82 $a=$sa+0.2; 83 while ($a<$ea) { 84 $xp = floor($width*cos($a)/2+$xc); 85 if ($a >= M_PI && $a <= 2*M_PI*1.01) { 86 $yp = floor($yc-($height*sin($a)/2)+$thick); 87 } else { 88 $yp = floor($yc-$height*sin($a)/2); 89 } 90 $coords.= ", $xp, $yp"; 91 $a += 0.2; 92 } 93 94 //Add the last point on the arc 95 $xp = floor($width*cos($ea)/2+$xc); 96 $yp = floor($yc-$height*sin($ea)/2); 97 98 99 if ($ea >= M_PI && $ea <= 2*M_PI*1.01) { 100 $coords.= ", $xp, ".floor($yp+$thick); 101 } 102 $coords.= ", $xp, $yp"; 103 if( !empty($this->csimalts[$i]) ) { 104 $tmp=sprintf($this->csimalts[$i],$this->data[$i]); 105 $alt="alt=\"$tmp\" title=\"$tmp\""; 106 } 107 if( !empty($this->csimtargets[$i]) ) 108 $this->csimareas .= "<area shape=\"poly\" coords=\"$coords\" href=\"".$this->csimtargets[$i]."\" $alt>\n"; 109 } 110 111 112 // Distance from the pie to the labels 113 function SetLabelMargin($m) { 114 assert($m>0 && $m<1); 115 $this->labelmargin=$m; 116 } 117 118 // Show a thin line from the pie to the label for a specific slice 119 function ShowLabelHint($f=true) { 120 $this->showlabelhint=$f; 121 } 122 123 // Set color of hint line to label for each slice 124 function SetLabelHintColor($c) { 125 $this->labelhintcolor=$c; 126 } 127 128 function SetHeight($aHeight) { 129 $this->iThickness = $aHeight; 130 } 131 132 133// Normalize Angle between 0-360 134 function NormAngle($a) { 135 // Normalize anle to 0 to 2M_PI 136 // 137 if( $a > 0 ) { 138 while($a > 360) $a -= 360; 139 } 140 else { 141 while($a < 0) $a += 360; 142 } 143 if( $a < 0 ) 144 $a = 360 + $a; 145 146 if( $a == 360 ) $a=0; 147 return $a; 148 } 149 150 151// Draw one 3D pie slice at position ($xc,$yc) with height $z 152 function Pie3DSlice($img,$xc,$yc,$w,$h,$sa,$ea,$z,$fillcolor, 153 $shadow=0.65,$edgecolor="",$arccolor="") { 154 155 dbgp( "s=$sa, e=$ea<br>\n" ); 156 157 $img->SetColor($fillcolor.":".$shadow); 158 for( $i=0; $i<$z; ++$i ) { 159 $img->CakeSlice($xc,$yc+$z-$i,$w,$h,360-$ea,360-$sa,$fillcolor.":".$shadow,"",3500); 160 } 161 if( $edgecolor == "" ) 162 $img->SetColor($fillcolor); 163 else 164 $img->SetColor($edgecolor); 165 $img->CakeSlice($xc,$yc+$z-$i,$w,$h,360-$ea,360-$sa,$fillcolor,$edgecolor ,2500); 166 167 } 168 169// Draw a 3D Pie 170 function Pie3D($img,$data,$colors,$xc,$yc,$d,$angle,$z, 171 $shadow=0.65,$startangle=0,$edgecolor="",$edgeweight=2) { 172 173 //--------------------------------------------------------------------------- 174 // As usual the algorithm get more complicated than I originally 175 // envisioned. I believe that this is as simple as it is possible 176 // to do it with the features I want. It's a good exercise to start 177 // thinking on how to do this to convince your self that all this 178 // is really needed for the general case. 179 // 180 // The algorithm two draw 3D pies without "real 3D" is done in 181 // two steps. 182 // First imagine the pie cut in half through a thought line between 183 // 12'a clock and 6'a clock. It now easy to imagine that we can plot 184 // the individual slices for each half by starting with the topmost 185 // pie slice and continue down to 6'a clock. 186 // 187 // In the algortithm this is done in three principal steps 188 // Step 1. Do the knife cut to ensure by splitting slices that extends 189 // over the cut line. This is done by splitting the original slices into 190 // upto 3 subslices. 191 // Step 2. Find the top slice for each half 192 // Step 3. Draw the slices from top to bottom 193 // 194 // The thing that slightly complicates this scheme with all the 195 // angle comparisons below is that we can have an arbitrary start 196 // angle so we must take into account the different equivalence classes. 197 // For the same reason we must walk through the angle array in a 198 // modulo fashion. 199 // 200 // Limitations of algorithm: 201 // * A small exploded slice which crosses the 270 degree point 202 // will get slightly nagged close to the center due to the fact that 203 // we print the slices in Z-order and that the slice left part 204 // get printed first and might get slightly nagged by a larger 205 // slice on the right side just before the right part of the small 206 // slice. Not a major problem though. 207 //--------------------------------------------------------------------------- 208 209 210 // Determine the height of the ellippse which gives an 211 // indication of the inclination angle 212 $h = ($angle/90.0)*$d; 213 $sum = 0; 214 for($i=0; $i<count($data); ++$i ) { 215 $sum += $data[$i]; 216 } 217 218 // Special optimization 219 if( $sum==0 ) return; 220 221 // Setup the start 222 $accsum = 0; 223 $a = $startangle; 224 $a = $this->NormAngle($a); 225 226 // 227 // Step 1 . Split all slices that crosses 90 or 270 228 // 229 $idx=0; 230 $adjexplode=array(); 231 for($i=0; $i<count($data); ++$i, ++$idx ) { 232 $da = $data[$i]/$sum * 360; 233 234 if( empty($this->explode_radius[$i]) ) 235 $this->explode_radius[$i]=0; 236 237 $la = $a + $da/2; 238 $explode = array( $xc + $this->explode_radius[$i]*cos($la*M_PI/180), 239 $yc - $this->explode_radius[$i]*sin($la*M_PI/180)*($h/$d) ); 240 $adjexplode[$idx] = $explode; 241 $labeldata[$i] = array($la,$explode[0],$explode[1]); 242 $originalangles[$i] = array($a,$a+$da); 243 244 $ne = $this->NormAngle($a+$da); 245 if( $da <= 180 ) { 246 // If the slice size is <= 90 it can at maximum cut across 247 // one boundary (either 90 or 270) where it needs to be split 248 dbgp( "da<=180, a=$a, ne=$ne, da=$da<br>" ); 249 $split=-1; // no split 250 if( ($da<=90 && ($a <= 90 && $ne > 90)) || 251 (($da <= 180 && $da >90) && (($a < 90 || $a >= 270) && $ne > 90)) ) { 252 dbgp( " a<=90 && ne>=90, a=$a, ne=$ne, da=$da<br>" ); 253 $split = 90; 254 } 255 elseif( ($da<=90 && ($a <= 270 && $ne > 270)) || 256 (($da<=180 && $da>90) && ($a >= 90 && $a < 270 && ($a+$da) > 270 )) ) { 257 dbgp( " a<=270 && ne>270, a=$a, ne=$ne, da=$da<br>" ); 258 $split = 270; 259 } 260 if( $split > 0 ) { // split in two 261 $angles[$idx] = array($a,$split); 262 $adjcolors[$idx] = $colors[$i]; 263 $adjexplode[$idx] = $explode; 264 $angles[++$idx] = array($split,$ne); 265 $adjcolors[$idx] = $colors[$i]; 266 $adjexplode[$idx] = $explode; 267 } 268 else { // no split 269 $angles[$idx] = array($a,$ne); 270 $adjcolors[$idx] = $colors[$i]; 271 $adjexplode[$idx] = $explode; 272 } 273 } 274 else { 275 // da>180 276 // Slice may, depending on position, cross one or two 277 // bonudaries 278 dbgp( "da<=180, a=$a, ne=$ne, da=$da, " ); 279 280 if( $a < 90 ) 281 $split = 90; 282 elseif( $a <= 270 ) 283 $split = 270; 284 else 285 $split = 90; 286 287 dbgp("split=$split<br>"); 288 289 $angles[$idx] = array($a,$split); 290 $adjcolors[$idx] = $colors[$i]; 291 $adjexplode[$idx] = $explode; 292 //if( $a+$da > 360-$split ) { 293 // For slices larger than 270 degrees we might cross 294 // another boundary as well. This means that we must 295 // split the slice further. The comparison gets a little 296 // bit complicated since we must take into accound that 297 // a pie might have a startangle >0 and hence a slice might 298 // wrap around the 0 angle. 299 // Three cases: 300 // a) Slice starts before 90 and hence gets a split=90, but 301 // we must also check if we need to split at 270 302 // b) Slice starts after 90 but before 270 and slices 303 // crosses 90 (after a wrap around of 0) 304 // c) If start is > 270 (hence the firstr split is at 90) 305 // and the slice is so large that it goes all the way 306 // around 270. 307 if( ($a < 90 && ($a+$da > 270)) || 308 ($a > 90 && $a<=270 && ($a+$da>360+90) ) || 309 ($a > 270 && $this->NormAngle($a+$da)>270) ) { 310 dbgp(" a+da > 360-$split, a=$a, da=$da<br>"); 311 $angles[++$idx] = array($split,360-$split); 312 $adjcolors[$idx] = $colors[$i]; 313 $adjexplode[$idx] = $explode; 314 $angles[++$idx] = array(360-$split,$ne); 315 $adjcolors[$idx] = $colors[$i]; 316 $adjexplode[$idx] = $explode; 317 } 318 else { 319 // Just a simple split to the previous decided 320 // angle. 321 $angles[++$idx] = array($split,$ne); 322 $adjcolors[$idx] = $colors[$i]; 323 $adjexplode[$idx] = $explode; 324 } 325 } 326 $a += $da; 327 $a = $this->NormAngle($a); 328 } 329 330 // Total number of slices 331 $n = count($angles); 332 333 dbgp("<br>Splitted pie:<br>"); 334 for($i=0; $i<$n; ++$i) { 335 list($dbgs,$dbge) = $angles[$i]; 336 dbgp(" #$i: s=$dbgs, e=$dbge<br>"); 337 } 338 339 // 340 // Step 2. Find start index (first pie that starts in upper left quadrant) 341 // 342 $minval = $angles[0][0]; 343 $min = 0; 344 for( $i=0; $i<$n; ++$i ) { 345 if( $angles[$i][0] < $minval ) { 346 $minval = $angles[$i][0]; 347 $min = $i; 348 } 349 } 350 $j = $min; 351 $cnt = 0; 352 while( $angles[$j][1] <= 90 ) { 353 $j++; 354 if( $j>=$n) { 355 $j=0; 356 } 357 if( $cnt > $n ) { 358 JpGraphError::Raise("Pie3D Internal error (#1). Trying to wrap twice when looking for start index"); 359 } 360 ++$cnt; 361 } 362 $start = $j; 363 dbgp( "Start index: $start<br>" ); 364 365 // 366 // Step 3. Print slices in z-order 367 // 368 $cnt = 0; 369 370 // First stroke all the slices between 90 and 270 (left half circle) 371 // counterclockwise 372 while( $angles[$j][0] < 270 ) { 373 374 list($x,$y) = $adjexplode[$j]; 375 376 $this->Pie3DSlice($img,$x,$y,$d,$h,$angles[$j][0],$angles[$j][1],$z,$adjcolors[$j], 377 $shadow); 378 379 $last = array($x,$y,$j); 380 381 $j++; 382 if( $j >= $n ) $j=0; 383 if( $cnt > $n ) { 384 JpGraphError::Raise("Pie3D Internal Error: Z-Sorting algorithm for 3D Pies is not working properly (2). Trying to wrap twice while stroking."); 385 } 386 ++$cnt; 387 } 388 389 $slice_left = $n-$cnt; 390 $j=$start-1; 391 if($j<0) $j=$n-1; 392 $cnt = 0; 393 394 // The stroke all slices from 90 to -90 (right half circle) 395 // clockwise 396 while( $cnt < $slice_left ) { 397 398 list($x,$y) = $adjexplode[$j]; 399 400 $this->Pie3DSlice($img,$x,$y,$d,$h,$angles[$j][0],$angles[$j][1],$z,$adjcolors[$j], 401 $shadow); 402 $j--; 403 if( $cnt > $n ) { 404 JpGraphError::Raise("Pie3D Internal Error: Z-Sorting algorithm for 3D Pies is not working properly (2). Trying to wrap twice while stroking."); 405 } 406 if($j<0) $j=$n-1; 407 $cnt++; 408 } 409 410 // Now do a special thing. Stroke the last slice on the left 411 // halfcircle one more time. This is needed in the case where 412 // the slice close to 270 have been exploded. In that case the 413 // part of the slice close to the center of the pie might be 414 // slightly nagged. 415 416 $this->Pie3DSlice($img,$last[0],$last[1],$d,$h,$angles[$last[2]][0],$angles[$last[2]][1],$z,$adjcolors[$last[2]],$shadow); 417 418 419 // Now print possible labels and add csim 420 $img->SetFont($this->value->ff,$this->value->fs); 421 $margin = $img->GetFontHeight()/2; 422 for($i=0; $i < count($data); ++$i ) { 423 $la = $labeldata[$i][0]; 424 $x = $labeldata[$i][1] + cos($la*M_PI/180)*($d+$margin); 425 $y = $labeldata[$i][2] - sin($la*M_PI/180)*($h+$margin); 426 if( $la > 180 && $la < 360 ) $y += $z; 427 if( $this->labeltype == 0 ) 428 if( $sum > 0 ) 429 $l = 100*$data[$i]/$sum; 430 else 431 $l = 0; 432 else 433 $l = $data[$i]; 434 435 $this->StrokeLabels($l,$img,$labeldata[$i][0]*M_PI/180,$x,$y); 436 437 $this->AddSliceToCSIM($i,$labeldata[$i][1],$labeldata[$i][2],$h*2,$d*2,$z, 438 $originalangles[$i][0],$originalangles[$i][1]); 439 } 440 441 // 442 // Finally add potential lines in pie 443 // 444 445 if( $edgecolor=="" ) return; 446 447 $accsum = 0; 448 $a = $startangle; 449 $a = $this->NormAngle($a); 450 451 $idx=0; 452 $img->PushColor($edgecolor); 453 454 455 $img->SetLineWeight($edgeweight); 456 for($i=0; $i < count($data); ++$i, ++$idx ) { 457 458 $x = $xc + floor(cos($a*M_PI/180) * $d); 459 $y = $yc - floor(sin($a*M_PI/180) * $h); 460 $img->Line($xc,$yc,$x,$y); 461 462 $da = $data[$i]/$sum * 360; 463 464 if( empty($this->explode_radius[$i]) ) 465 $this->explode_radius[$i]=0; 466 467 $la = $a + $da/2; 468 $explode = array( $xc + $this->explode_radius[$i]*cos($la*M_PI/180), 469 $yc - $this->explode_radius[$i]*sin($la*M_PI/180)*($h/$d) ); 470 471 $a += $da; 472 } 473 474 $img->SetLineWeight(2); 475 476 // Right sideline 477 $img->Line($xc+$d,$yc,$xc+$d,$yc+$z); 478 479 // Left sideline 480 $img->Line($xc-$d+1,$yc,$xc-$d+1,$yc+$z); 481 482 // Major full ellipse 483 $img->Ellipse($xc,$yc+1,$d*2.01,$h*2.01); 484 $img->Ellipse($xc+1,$yc,$d*2.01,$h*2.01); 485 $img->Ellipse($xc,$yc,$d*2.01,$h*2.01); 486 487 // Lower half ellipse 488 $img->Arc($xc,$yc+$z,$d*2,$h*2,0,180); 489 $img->Arc($xc,$yc+$z+1,$d*2,$h*2,0,180); 490 491 $img->PopColor(); 492 $img->SetLineWeight(1); 493 } 494 495 496 function Stroke($img) { 497 498 $colors = array_keys($img->rgb->rgb_table); 499 sort($colors); 500 501 if( $this->setslicecolors==null ) { 502 $idx_a=$this->themearr[$this->theme]; 503 $numcolors = count($idx_a); 504 $ca = array(); 505 for($i=0; $i<$numcolors; ++$i) 506 $ca[$i] = $colors[$idx_a[$i]]; 507 } 508 else { 509 $ca = $this->setslicecolors; 510 } 511 512 $numcolors=count($ca); 513 514 $xc = $this->posx*$img->width; 515 $yc = $this->posy*$img->height; 516 517 if( $this->radius < 1 ) { 518 $width = floor($this->radius*min($img->width,$img->height)); 519 // Make sure that the pie doesn't overflow the image border 520 // The 0.9 factor is simply an extra margin to leave some space 521 // between the pie an the border of the image. 522 $width = min($width,min($xc*0.9,($yc*90/$this->angle-$width/4)*0.9)); 523 } 524 else 525 $width = $this->radius ; 526 527 // Add a sanity check for width 528 if( $width < 1 ) { 529 JpGraphError::Raise("Width for 3D Pie is 0. Specify a size > 0"); 530 exit(); 531 } 532 533 // Establish a thickness. By default the thickness is a fifth of the 534 // pie slice width (=pie radius) but since the perspective depends 535 // on the inclination angle we use some heuristics to make the edge 536 // slightly thicker the less the angle. 537 538 // Has user specified an absolute thickness? In that case use 539 // that instead 540 if( $this->iThickness ) 541 $thick = $this->iThickness; 542 else 543 $thick = $width/7; 544 $a = $this->angle; 545 if( $a <= 30 ) $thick *= 1.6; 546 elseif( $a <= 40 ) $thick *= 1.4; 547 elseif( $a <= 50 ) $thick *= 1.2; 548 elseif( $a <= 60 ) $thick *= 1.0; 549 elseif( $a <= 70 ) $thick *= 0.8; 550 elseif( $a <= 80 ) $thick *= 0.7; 551 else $thick *= 0.6; 552 553 $thick = floor($thick); 554 555 if( $this->explode_all ) 556 for($i=0;$i<count($this->data);++$i) 557 $this->explode_radius[$i]=$this->explode_r; 558 559 $this->Pie3D($img,$this->data, $ca, $xc, $yc, $width, $this->angle, 560 $thick, 0.65, $this->startangle, $this->edgecolor, $this->edgeweight); 561 562 // Adjust title position 563 $this->title->Pos($xc,$yc-$this->title->GetFontHeight($img)-$width/2-$this->title->margin, 564 "center","bottom"); 565 $this->title->Stroke($img); 566 } 567 568//--------------- 569// PRIVATE METHODS 570 571 // Position the labels of each slice 572 function StrokeLabels($label,$img,$a,$xp,$yp) { 573 $this->value->halign="left"; 574 $this->value->valign="top"; 575 $this->value->margin=0; 576 577 // Position the axis title. 578 // dx, dy is the offset from the top left corner of the bounding box that sorrounds the text 579 // that intersects with the extension of the corresponding axis. The code looks a little 580 // bit messy but this is really the only way of having a reasonable position of the 581 // axis titles. 582 $img->SetFont($this->value->ff,$this->value->fs,$this->value->fsize); 583 $h=$img->GetTextHeight($label); 584 $w=$img->GetTextWidth(sprintf($this->value->format,$label)); 585 while( $a > 2*M_PI ) $a -= 2*M_PI; 586 if( $a>=7*M_PI/4 || $a <= M_PI/4 ) $dx=0; 587 if( $a>=M_PI/4 && $a <= 3*M_PI/4 ) $dx=($a-M_PI/4)*2/M_PI; 588 if( $a>=3*M_PI/4 && $a <= 5*M_PI/4 ) $dx=1; 589 if( $a>=5*M_PI/4 && $a <= 7*M_PI/4 ) $dx=(1-($a-M_PI*5/4)*2/M_PI); 590 591 if( $a>=7*M_PI/4 ) $dy=(($a-M_PI)-3*M_PI/4)*2/M_PI; 592 if( $a<=M_PI/4 ) $dy=(1-$a*2/M_PI); 593 if( $a>=M_PI/4 && $a <= 3*M_PI/4 ) $dy=1; 594 if( $a>=3*M_PI/4 && $a <= 5*M_PI/4 ) $dy=(1-($a-3*M_PI/4)*2/M_PI); 595 if( $a>=5*M_PI/4 && $a <= 7*M_PI/4 ) $dy=0; 596 597 $this->value->Stroke($img,$label,$xp-$dx*$w,$yp-$dy*$h); 598 } 599} // Class 600 601/* EOF */ 602?> 603