Avail feature updated
[ninja.git] / application / vendor / mfchart / Chart.php
blob2b93b7e02f8dff6ddcddd35cec510a76b48c8bee
1 <?php
3 #define("FONT_TAHOMA", 'fonts/tahoma.ttf');
4 #define("FONT_DEJAVUSANS", 'fonts/DejaVuSans.ttf');
5 #define("FONT_DEJAVUSANS_CONDENSED", 'fonts/DejaVuSansCondensed.ttf');
6 #define("FONT_DEJAVUSERIF", 'fonts/DejaVuSerif.ttf');
7 #define("FONT_DEJAVUSERIF_CONDENSED", 'fonts/DejaVuSerifCondensed.ttf');
9 #define("FONT_TAHOMA", Kohana::find_file('fonts', 'tahoma', 'ttf'));
10 #define("FONT_DEJAVUSANS", Kohana::find_file('fonts', 'DejaVuSans', 'ttf'));
11 #define("FONT_DEJAVUSANS_CONDENSED", Kohana::find_file('vendor', 'mfchart/fonts/DejaVuSansCondensed', false, 'ttf'));
12 #define("FONT_DEJAVUSERIF", Kohana::find_file('fonts', 'DejaVuSerif', 'ttf'));
13 #define("FONT_DEJAVUSERIF_CONDENSED", Kohana::find_file('fonts', 'DejaVuSerifCondensed', 'ttf'));
16 class Chart {
18 protected $image; // image with the graph
20 protected $width = 450; // width of the graph
22 protected $height = 180; // height of the graph
24 protected $margin_left = 0; // margin around the graph where the legend is displayed
26 protected $margin_bottom = 0; // margin around the graph where the legend is displayed
28 protected $margin_right = 0; // margin around the graph where the legend is displayed
30 protected $margin_top = 0; // margin around the graph where the legend is displayed
32 protected $graph_width; // graph plot area
34 protected $graph_height; // graph plot area
36 protected $values = array(); // values for displaying in the graph
38 protected $font = FONT_DEJAVUSANS_CONDENSED; // hardcoded for now
40 protected $font_size = 8; // GD2 - points (but GD1 - pixels)
42 protected $colors = array(); // for colors init -- items are arrays in format array(hex_color, alpha, allocated object) or array(array(hex_color, alpha, allocated object), ...)
44 protected $title = '';
46 protected $legend = array(); // legend for the graph
48 protected $occupied_areas = array(); // for collision detect when placing some items like legend box or labels for points in line graphs etc.
49 // each item in format array(x1, y1, x2, y2)
51 public function __construct($width=NULL, $height=NULL)
53 if ($width != NULL)
54 $this->width = (int) $width;
55 if ($height != NULL)
56 $this->height = (int) $height;
58 $this->colors['background_color'] = array('#fdfdfd', NULL, NULL); // background of the generated image
59 #$this->colors['background_color'] = array('#f6f7f8', NULL, NULL); // background of the generated image
60 $this->colors['border_color'] = array('#fdfdfd', NULL, NULL); // border of the generated image
61 #$this->colors['border_color'] = array(NULL, NULL, NULL); // border of the generated image
62 $this->colors['font_color'] = array('#595959', NULL, NULL); // values at axis
63 $this->colors['font_color2'] = array('#000000', NULL, NULL); // legend at axis & legend
64 $this->colors['font_color3'] = array('#d08a22', NULL, NULL); // values at bars
65 $this->colors['legend_color'] = array('#fefefe', NULL, NULL); // background legend color
66 $this->colors['shade_color'] = array('#666666', 95, NULL);
67 $this->colors['shade_color2'] = array('#ffffff', NULL, NULL);
68 $this->colors['shade_color3'] = array('#ffffff', NULL, NULL);
69 # $this->colors['shade_color2'] = array('#e6e6e6', NULL, NULL);
70 # $this->colors['shade_color3'] = array('#f0f0f0', NULL, NULL);
73 /**
74 * You can set one margin for all by setting only firs parameter.
75 * Or you can set vertical and horizontal margin by setting two parameters.
76 * Or all four...
78 * @param int left margin
79 * @param int top margin
80 * @param int right margin
81 * @param int bottom margin
82 **/
83 public function set_margins($left, $top=NULL, $right=NULL, $bottom=NULL)
85 $this->margin_left = $left;
86 $this->margin_top = ($top === NULL) ? $this->margin_left : $top;
87 $this->margin_right = ($right === NULL) ? $this->margin_left : $right;
88 $this->margin_bottom = ($bottom === NULL) ? $this->margin_top : $bottom;
91 public function set_font($font)
93 $this->font = $font;
96 public function set_font_size($size)
98 $this->font_size = $size;
101 public function set_legend($values)
103 $this->legend = $values;
106 public function set_color($color_name, $color_value, $alpha=NULL)
108 $this->colors[$color][0] = $color_value;
109 if ($alpha !== NULL)
110 $this->colors[$color][1] = $alpha;
113 public function set_title($value)
115 $this->title = $value;
118 // set the values for the graph
119 public function set_data($data, $type=false)
122 $labels = array();
124 // to float - "check"
125 foreach ($data as $key => $row)
127 $key = preg_replace("~[^a-zA-Z0-9 -_]~", null, $key);
128 if ($type === 'pie')
129 $labels[] = $key;
131 if (is_array($row))
133 foreach ($row as $k => $v)
134 $data[$key][$k] = (float) $v;
136 else
138 $data[$key] = (float) $row;
142 if ($type === 'pie') {
143 unset($this->colors['colors']);
144 $this->colors['colors'] = reports::get_color_values($labels);
147 $this->values = $data;
150 // draw image with the graph
151 public function draw()
153 $this->image = imagecreatetruecolor($this->width, $this->height);
154 imagealphablending($this->image, true);
156 $this->init_colors();
158 // draw background + border
159 if ($this->get_color('border_color'))
161 imagefilledrectangle($this->image, 0, 0, $this->width, $this->height, $this->get_color('border_color')); // draw border
162 imagefilledrectangle($this->image, 1, 1, $this->width-2, $this->height-2, $this->get_color('background_color')); // draw background
164 else
166 imagefilledrectangle($this->image, 0, 0, $this->width, $this->height, $this->get_color('background_color')); // draw background
169 // draw title
170 if (!empty($this->title))
172 $box_points = imagettfbbox($this->font_size, 0, $this->font, $this->title);
173 $textheight = $box_points[3]-$box_points[5];
174 $textwidth = $box_points[4]-$box_points[6];
175 utilities::imagestringbox($this->image, $this->font, $this->font_size, 0, $this->margin_top, $this->width, $this->margin_top+$textheight, ALIGN_CENTER, VALIGN_MIDDLE, 0, $this->title, $this->get_color('font_color2'));
176 $this->margin_top += $textheight + 10;
178 $this->add_occupied($this->width/2-$textwidth/2, $this->margin_top, $this->width/2+$textwidth/2, $this->margin_top+$textheight);
182 // get the soruce of the image
183 public function get_image($type = 'png')
185 ob_start();
186 switch ($type)
188 case 'png':
189 imagepng($this->image);
190 break;
192 case 'jpg':
193 case 'jpeg':
194 imagejpeg($this->image, '', 0.7);
195 break;
197 case 'gif':
198 imagegif($this->image);
199 break;
201 case 'wbmp':
202 imagewbmp($this->image);
203 break;
205 $img = ob_get_contents();
206 ob_end_clean();
208 return $img;
211 // display the graph
212 public function display()
214 if (function_exists("imagepng"))
216 header("Content-type: image/png");
217 echo $this->get_image('png');
219 elseif (function_exists("imagejpeg"))
221 header("Content-type: image/jpeg");
222 echo $this->get_image('jpg');
224 elseif (function_exists("imagegif"))
226 header("Content-type: image/gif");
227 echo $this->get_image('gif');
229 elseif (function_exists("imagewbmp"))
231 header("Content-type: image/vnd.wap.wbmp");
232 echo $this->get_image('wbmp');
234 else
236 throw new Exception("Doh! No graphical functions on this server?");
239 return true;
242 public function set_width($w)
244 $this->width = $w;
247 public function set_height($h)
249 $this->height = $h;
253 // returns size of the graph plot area
254 protected function get_plot_area()
256 return array(
257 $this->width - $this->margin_left - $this->margin_right, // width
258 $this->height - $this->margin_top - $this->margin_bottom // height
262 protected function get_color($key, $i=0, $j=2)
264 if (!isset($this->colors[$key]))
265 return FALSE;
267 $i = $i % count($this->colors[$key]); // if there is not enough colors get it in cycle
269 if ($this->colors[$key][$i][$j] === NULL)
270 return FALSE;
272 return $this->colors[$key][$i][$j];
276 // init colors
277 private function init_colors()
279 foreach ($this->colors as $key => $values)
281 if (!is_array($values[0]))
282 $this->colors[$key] = $values = array($values);
284 foreach ($values as $i => $value)
286 if ($value[0] === NULL)
287 continue;
289 $rgb = utilities::hex2rgb($value[0]);
291 if ($value[1] === NULL)
292 $this->colors[$key][$i][2] = imagecolorallocate($this->image, $rgb[0], $rgb[1], $rgb[2]);
293 else
294 // alpha
295 $this->colors[$key][$i][2] = imagecolorallocatealpha($this->image, $rgb[0], $rgb[1], $rgb[2], $value[1]);
301 * Draw the legend box.
302 * If position of the box isn't set manually it's trying to determine position automatically according to already occupied areas.
304 * @param str Color of the box background.
305 * @param int Relative size of font in points. The font size. Depending on your version of GD, this should be specified as the pixel size (GD1) or point size (GD2).
306 * @param int Position x of the top left of the box.
307 * @param int Position y of the top left of the box.
309 protected function draw_legend($color_index, $relative_font_size=-1, $position_x=NULL, $position_y=NULL)
311 if (empty($this->legend))
312 return;
314 $font_legend = ($relative_font_size<0 AND $this->font_size<abs($relative_font_size)) ? $this->font_size : $this->font_size+$relative_font_size;
316 $maxwidth = 0;
317 $maxheight = 0;
318 foreach ($this->legend as $l)
320 $box_points = imagettfbbox($font_legend, 0, $this->font, $l);
321 $width = $box_points[4]-$box_points[6];
322 $height = $box_points[3]-$box_points[5];
323 if ($maxwidth < $width)
324 $maxwidth = $width;
325 if ($maxheight < $height)
326 $maxheight = $height;
328 $maxheight += $maxheight*0.3; // line spacing
330 $border = $maxheight*0.5;
332 if ($position_x === NULL AND $position_y === NULL)
334 $from = array($this->margin_left+5, $this->margin_top-10 > 0 ? $this->margin_top-10 : 0);
335 $to = array($this->width, $this->height-$this->margin_bottom);
336 $found = $this->place_in_free($maxwidth+$border*2+10, count($this->legend)*$maxheight+$border*2, 5, $from, $to);
337 $position_x = $found[0];
338 $position_y = $found[1];
341 $legend_x1 = $position_x;
342 $legend_x2 = $position_x + $maxwidth + $border*2 + 10;
343 $legend_y1 = $position_y;
344 $legend_y2 = $position_y + count($this->legend)*$maxheight + $border*2;
346 // $legend_x1 = $this->width-$maxwidth-40;
347 // $legend_x2 = $legend_x1 + $maxwidth + 30;
348 // $legend_y1 = $this->margin_top-10;
349 // $legend_y2 = $legend_y1 + count($this->legend)*$maxheight + 10;
351 utilities::imagefillroundedrect($this->image, $legend_x1+2, $legend_y1+2, $legend_x2+2, $legend_y2+2, 5, $this->get_color('shade_color3'));
352 utilities::imagefillroundedrect($this->image, $legend_x1+1, $legend_y1+1, $legend_x2+1, $legend_y2+1, 5, $this->get_color('shade_color2'));
353 utilities::imagefillroundedrect($this->image, $legend_x1, $legend_y1, $legend_x2, $legend_y2, 5, $this->get_color('legend_color'));
355 $i = 0;
356 $yi = 0;
357 foreach ($this->legend as $l)
359 $color = $this->get_color($color_index, $i);
360 if( $color !== false ) {
361 $y = $legend_y1 + 5 + $maxheight*$yi;
362 imagefilledrectangle($this->image, $legend_x1+$border, $y+$maxheight/2-2, $legend_x1+$border+5, $y+$maxheight/2+3, $color);
363 utilities::imagestringbox($this->image, $this->font, $font_legend, $legend_x1+$border+10, $y, $legend_x2, $y+$maxheight, ALIGN_LEFT, VALIGN_MIDDLE, 0, $l, $this->get_color('font_color2'));
364 $yi++;
366 $i++;
370 protected function add_occupied($x1, $y1, $x2, $y2)
372 // order it
373 if ($x1 > $x2)
374 list($x1, $x2) = array($x2, $x1);
375 if ($y1 > $y2)
376 list($y1, $y2) = array($y2, $y1);
378 $this->occupied_areas[] = array(
379 round($x1),
380 round($y1),
381 round($x2),
382 round($y2)
387 * Detect collision of two boxes. Try to find free place where the box could be placed.
388 * Searching in columns priority now. Returns the solution which is the closest to the top or bottom border in the first column where some solution has beend found.
389 * It's brute-force. I think it could be optimised when some ordering is used for $occupied_areas.
390 * Supports only rectangles aligned with x and y axis.
392 * NOTE: For use also with rotated rectangles (for lines for example) look at "separating axis test" at Internet and implement it :)
393 * http://en.wikipedia.org/wiki/Separating_axis_theorem
394 * http://board.flashkit.com/board/showthread.php?t=787281
396 * @param int Width of the box.
397 * @param int Height of the box.
398 * @param int Padding from the collision boxes. Working correctly only for the last found collision now :((
399 * @param array Where should the searching start? Point [x,y].
400 * @param array Where should the searching end? Point [x,y].
401 * @param [left,right] Direction of searching in x axis.
402 * @param [up,down] Direction of searching in y axis.
404 protected function place_in_free($width, $height, $padding=5, $from=array(0,0), $to=array(NULL,NULL), $direction_x = 'left', $direction_y = 'down')
406 $width += $padding*2; // consider padding
407 $height += $padding*2; // consider padding
409 // if it's not specified set it to the right bottom corner of the image
410 if ($to[0] === NULL)
411 $to[0] = $this->width;
412 if ($to[1] === NULL)
413 $to[1] = $this->height;
415 // order from and to points
416 if ($from[0] > $to[0])
417 list($from[0], $to[0]) = array($to[0], $from[0]);
418 if ($from[1] > $to[1])
419 list($from[1], $to[1]) = array($to[1], $from[1]);
421 // init searched box
422 if ($direction_x == 'left')
424 $step_x = -1;
425 $x1 = $to[0]-$width;
426 $x2 = $to[0];
428 else
430 $step_x = 1;
431 $x1 = $from[0];
432 $x2 = $from[0]+$width;
434 if ($direction_y == 'up')
436 $step_y = -1;
437 $y1 = $to[1]-$height;
438 $y2 = $to[1];
440 else
442 $step_y = 1;
443 $y1 = $from[1];
444 $y2 = $from[1]+$height;
447 $found = NULL;
448 $intersect = TRUE;
449 while ($intersect)
451 $intersect = FALSE;
452 foreach ($this->occupied_areas as $occupied) // cycle through all occupied areas - brute-force
454 if ($x2>=$occupied[0] AND $x1<=$occupied[2] AND $y1<=$occupied[3] AND $y2>=$occupied[1])
455 { // collision of boxes
456 $intersect = TRUE;
458 // step for axis y movement
459 if ($direction_y == 'down')
460 $sy = ($occupied[3]-$y1+1)*$step_y;
461 else
462 $sy = abs($occupied[1]-$y2+1)*$step_y;
464 if ($direction_y == 'down' AND $y2+abs($sy) > $to[1])
465 { // go to the next column
466 if ($found !== NULL) // there is already found solution
467 break 2;
468 $y1 = $from[1];
469 $y2 = $from[1]+$height;
470 $x1 += $step_x;
471 $x2 += $step_x;
473 elseif ($direction_y == 'up' AND $y1-abs($sy) < $from[1])
474 { // go to the next column
475 if ($found !== NULL) // there is already found solution
476 break 2;
477 $y1 = $to[1]-$height;
478 $y2 = $to[1];
479 $x1 += $step_x;
480 $x2 += $step_x;
482 else
484 $y1 += $sy;
485 $y2 += $sy;
488 if ($x1<$from[0] OR $x2>$to[0]) // we are at the borders (left or right) and nothing found, go out
489 break 2;
491 break;
495 if (!$intersect) // found solution
496 { // store found solution and try to look for better one in this column
497 if ($found === NULL)
498 { // first found
499 $found = array($x1, $y1, $x2, $y2);
501 else
502 { // replace previous solution only if it's more closely to the border (top or bottom)
503 $gaps = array($found[1]-$from[1], $to[1]-$found[3], $y1-$from[1], $to[1]-$y2);
504 if (($gaps[2]<$gaps[0] AND $gaps[2]<$gaps[1]) OR ($gaps[3]<$gaps[0] AND $gaps[3]<$gaps[1]))
505 $found = array($x1, $y1, $x2, $y2);
508 // simulate intersection
509 $intersect = TRUE;
511 // move the box
512 if (($direction_y == 'down' AND $y2+$step_y > $to[1]) OR ($direction_y == 'up' AND $y1-$step_y < $from[1])) // border found
513 break;
515 $y1 += $step_y;
516 $y2 += $step_y;
520 if ($found !== NULL) // return found solution
521 list($x1, $y1, $x2, $y2) = $found;
523 return array($x1, $y1, $x2, $y2);
527 // This one is for future purpuses if anything needs to be rewritten. Maybe it would be helpful.
528 // There is approach without switching in the code and also with switching. It's badly mixed...
530 // Switched 'left' and 'right'. Need to be corrected.
532 // protected function place_in_free($width, $height, $padding=5, $from=array(0,0), $to=array(NULL,NULL), $direction_x = 'left', $direction_y = 'down')
533 // {
534 // $width += $padding*2; // consider padding
535 // $height += $padding*2; // consider padding
537 // // if it's not specified set it to the right bottom corner of the image
538 // if ($to[0] === NULL)
539 // $to[0] = $this->width;
540 // if ($to[1] === NULL)
541 // $to[1] = $this->height;
543 // // order from and to points
544 // if ($from[0] > $to[0])
545 // list($from[0], $to[0]) = array($to[0], $from[0]);
546 // if ($from[1] > $to[1])
547 // list($from[1], $to[1]) = array($to[1], $from[1]);
549 // // init searched box
550 // if ($direction_x == 'right')
551 // {
552 // $step_x = -1;
553 // $x1 = $to[0]-$width;
554 // $x2 = $to[0];
555 // }
556 // else
557 // {
558 // $step_x = 1;
559 // $x1 = $from[0];
560 // $x2 = $from[0]+$width;
561 // }
562 // if ($direction_y == 'up')
563 // {
564 // $step_y = -1;
565 // $y1 = $to[1]-$height;
566 // $y2 = $to[1];
567 // }
568 // else
569 // {
570 // $step_y = 1;
571 // $y1 = $from[1];
572 // $y2 = $from[1]+$height;
573 // }
575 // $switch = FALSE; // used for switching between upper and bottom border so it's searching from the borders to the vertical center
577 // $intersect = TRUE;
578 // while ($intersect)
579 // {
580 // $intersect = FALSE;
581 // foreach ($this->occupied_areas as $occupied) // cycle through all occupied areas - brute-force
582 // {
583 // if ($x2>=$occupied[0] AND $x1<=$occupied[2] AND $y1<=$occupied[3] AND $y2>=$occupied[1])
584 // { // collision of boxes
585 // $intersect = TRUE;
587 // $sx = ($occupied[2]-$occupied[0])*$step_x; // step for axis x movement
589 // // move the box
590 // if ($direction_x == 'left' AND $x2+abs($sx) > $to[0])
591 // {
592 // $x1 = $from[0];
593 // $x2 = $from[0]+$width;
594 // $y1 += $step_y;
595 // $y2 += $step_y;
596 // }
597 // elseif ($direction_x == 'right' AND $x1-abs($sx) < $from[0])
598 // {
599 // $x1 = $to[0]-$width;
600 // $x2 = $to[0];
602 // if (($direction_y == 'down' AND !$switch) OR ($direction_y == 'up' AND !$switch))
603 // { // switch to the bottom border
604 // $gap = $y1-$from[1]; // vertical gap
605 // $y2 = $to[1]-$gap;
606 // $y1 = $y2-$height;
607 // }
608 // elseif (($direction_y == 'down' AND $switch) OR ($direction_y == 'up' AND !$switch))
609 // { // switch to the upper border
610 // $gap = $to[1]-$y2; // vertical gap
611 // $y1 = $from[0]+$gap;
612 // $y2 = $y1+$height;
613 // }
615 // if (round($gap) == round($height/2)) // we are at the vertical center and nothing has been found
616 // break 2;
618 // if ($switch) // switch to the other side (switching between top and down and going to the vertical center)
619 // {
620 // $y1 += $step_y;
621 // $y2 += $step_y;
622 // }
623 // $switch = !$switch;
624 // }
625 // else
626 // {
627 // $x1 += $sx;
628 // $x2 += $sx;
629 // }
631 // // if we don't have a way to go -- this is useless now when searching from borders to the vertical center
632 // // if (($direction_y == 'up' AND $y1 <= $from[1]) OR ($direction_y == 'down' AND $y2 >= $to[1]))
633 // // break 2;
635 // break;
636 // }
637 // }
638 // }
640 // // if ($intersect) // nothing found
641 // // return $base;
643 // return array($x1, $y1, $x2, $y2);
644 // }