Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
/lib/ -> graphlib.php (source)

Differences Between: [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   1  <?php
   2  
   3  /**
   4   * Graph Class. PHP Class to draw line, point, bar, and area graphs, including numeric x-axis and double y-axis.
   5   * Version: 1.6.3
   6   * Copyright (C) 2000  Herman Veluwenkamp
   7   *
   8   * This library is free software; you can redistribute it and/or
   9   * modify it under the terms of the GNU Lesser General Public
  10   * License as published by the Free Software Foundation; either
  11   * version 2.1 of the License, or (at your option) any later version.
  12   *
  13   * This library is distributed in the hope that it will be useful,
  14   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  16   * Lesser General Public License for more details.
  17   *
  18   * You should have received a copy of the GNU Lesser General Public
  19   * License along with this library; if not, write to the Free Software
  20   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  21   *
  22   * Copy of GNU Lesser General Public License at: http://www.gnu.org/copyleft/lesser.txt
  23   * Contact author at: hermanV@mindless.com
  24   *
  25   * @package    core
  26   * @subpackage lib
  27   */
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  /* This file contains modifications by Martin Dougiamas
  32   * as part of Moodle (http://moodle.com).  Modified lines
  33   * are marked with "Moodle".
  34   */
  35  
  36  /**
  37   * @package moodlecore
  38   */
  39  class graph {
  40    var $image;
  41    var $debug             =   FALSE;        // be careful!!
  42    var $calculated        =   array();      // array of computed values for chart
  43    var $parameter         =   array(        // input parameters
  44      'width'              =>  320,          // default width of image
  45      'height'             =>  240,          // default height of image
  46      'file_name'          => 'none',        // name of file for file to be saved as.
  47                                             //  NOTE: no suffix required. this is determined from output_format below.
  48      'output_format'      => 'PNG',         // image output format. 'GIF', 'PNG', 'JPEG'. default 'PNG'.
  49  
  50      'seconds_to_live'    =>  0,            // expiry time in seconds (for HTTP header)
  51      'hours_to_live'      =>  0,            // expiry time in hours (for HTTP header)
  52      'path_to_fonts'      => 'fonts/',      // path to fonts folder. don't forget *trailing* slash!!
  53                                             //   for WINDOZE this may need to be the full path, not relative.
  54  
  55      'title'              => 'Graph Title', // text for graph title
  56      'title_font'         => 'default.ttf',   // title text font. don't forget to set 'path_to_fonts' above.
  57      'title_size'         =>  16,           // title text point size
  58      'title_colour'       => 'black',       // colour for title text
  59  
  60      'x_label'            => '',            // if this is set then this text is printed on bottom axis of graph.
  61      'y_label_left'       => '',            // if this is set then this text is printed on left axis of graph.
  62      'y_label_right'      => '',            // if this is set then this text is printed on right axis of graph.
  63  
  64      'label_size'         =>  8,           // label text point size
  65      'label_font'         => 'default.ttf', // label text font. don't forget to set 'path_to_fonts' above.
  66      'label_colour'       => 'gray33',      // label text colour
  67      'y_label_angle'      =>  90,           // rotation of y axis label
  68  
  69      'x_label_angle'      =>  90,            // rotation of y axis label
  70  
  71      'outer_padding'      =>  5,            // padding around outer text. i.e. title, y label, and x label.
  72      'inner_padding'      =>  0,            // padding beteen axis text and graph.
  73      'x_inner_padding'      =>  5,            // padding beteen axis text and graph.
  74      'y_inner_padding'      =>  6,            // padding beteen axis text and graph.
  75      'outer_border'       => 'none',        // colour of border aound image, or 'none'.
  76      'inner_border'       => 'black',       // colour of border around actual graph, or 'none'.
  77      'inner_border_type'  => 'box',         // 'box' for all four sides, 'axis' for x/y axis only,
  78                                             // 'y' or 'y-left' for y axis only, 'y-right' for right y axis only,
  79                                             // 'x' for x axis only, 'u' for both left and right y axis and x axis.
  80      'outer_background'   => 'none',        // background colour of entire image.
  81      'inner_background'   => 'none',        // background colour of plot area.
  82  
  83      'y_min_left'         =>  0,            // this will be reset to minimum value if there is a value lower than this.
  84      'y_max_left'         =>  0,            // this will be reset to maximum value if there is a value higher than this.
  85      'y_min_right'        =>  0,            // this will be reset to minimum value if there is a value lower than this.
  86      'y_max_right'        =>  0,            // this will be reset to maximum value if there is a value higher than this.
  87      'x_min'              =>  0,            // only used if x axis is numeric.
  88      'x_max'              =>  0,            // only used if x axis is numeric.
  89  
  90      'y_resolution_left'  =>  1,            // scaling for rounding of y axis max value.
  91                                             // if max y value is 8645 then
  92                                             // if y_resolution is 0, then y_max becomes 9000.
  93                                             // if y_resolution is 1, then y_max becomes 8700.
  94                                             // if y_resolution is 2, then y_max becomes 8650.
  95                                             // if y_resolution is 3, then y_max becomes 8645.
  96                                             // get it?
  97      'y_decimal_left'     =>  0,            // number of decimal places for y_axis text.
  98      'y_resolution_right' =>  2,            // ... same for right hand side
  99      'y_decimal_right'    =>  0,            // ... same for right hand side
 100      'x_resolution'       =>  2,            // only used if x axis is numeric.
 101      'x_decimal'          =>  0,            // only used if x axis is numeric.
 102  
 103      'point_size'         =>  4,            // default point size. use even number for diamond or triangle to get nice look.
 104      'brush_size'         =>  4,            // default brush size for brush line.
 105      'brush_type'         => 'circle',      // type of brush to use to draw line. choose from the following
 106                                             //   'circle', 'square', 'horizontal', 'vertical', 'slash', 'backslash'
 107      'bar_size'           =>  0.8,          // size of bar to draw. <1 bars won't touch
 108                                             //   1 is full width - i.e. bars will touch.
 109                                             //   >1 means bars will overlap.
 110      'bar_spacing'        =>  10,           // space in pixels between group of bars for each x value.
 111      'shadow_offset'      =>  3,            // draw shadow at this offset, unless overidden by data parameter.
 112      'shadow'             => 'grayCC',      // 'none' or colour of shadow.
 113      'shadow_below_axis'  => true,         // whether to draw shadows of bars and areas below the x/zero axis.
 114  
 115  
 116      'x_axis_gridlines'   => 'auto',        // if set to a number then x axis is treated as numeric.
 117      'y_axis_gridlines'   =>  6,            // number of gridlines on y axis.
 118      'zero_axis'          => 'none',        // colour to draw zero-axis, or 'none'.
 119  
 120  
 121      'axis_font'          => 'default.ttf', // axis text font. don't forget to set 'path_to_fonts' above.
 122      'axis_size'          =>  8,            // axis text font size in points
 123      'axis_colour'        => 'gray33',      // colour of axis text.
 124      'y_axis_angle'       =>  0,            // rotation of axis text.
 125      'x_axis_angle'       =>  0,            // rotation of axis text.
 126  
 127      'y_axis_text_left'   =>  1,            // whether to print left hand y axis text. if 0 no text, if 1 all ticks have text,
 128      'x_axis_text'        =>  1,            //   if 4 then print every 4th tick and text, etc...
 129      'y_axis_text_right'  =>  0,            // behaviour same as above for right hand y axis.
 130  
 131      'x_offset'           =>  0.5,          // x axis tick offset from y axis as fraction of tick spacing.
 132      'y_ticks_colour'     => 'black',       // colour to draw y ticks, or 'none'
 133      'x_ticks_colour'     => 'black',       // colour to draw x ticks, or 'none'
 134      'y_grid'             => 'line',        // grid lines. set to 'line' or 'dash'...
 135      'x_grid'             => 'line',        //   or if set to 'none' print nothing.
 136      'grid_colour'        => 'grayEE',      // default grid colour.
 137      'tick_length'        =>  4,            // length of ticks in pixels. can be negative. i.e. outside data drawing area.
 138  
 139      'legend'             => 'none',        // default. no legend.
 140                                            // otherwise: 'top-left', 'top-right', 'bottom-left', 'bottom-right',
 141                                            //   'outside-top', 'outside-bottom', 'outside-left', or 'outside-right'.
 142      'legend_offset'      =>  10,           // offset in pixels from graph or outside border.
 143      'legend_padding'     =>  5,            // padding around legend text.
 144      'legend_font'        => 'default.ttf',   // legend text font. don't forget to set 'path_to_fonts' above.
 145      'legend_size'        =>  8,            // legend text point size.
 146      'legend_colour'      => 'black',       // legend text colour.
 147      'legend_border'      => 'none',        // legend border colour, or 'none'.
 148  
 149      'decimal_point'      => '.',           // symbol for decimal separation  '.' or ',' *european support.
 150      'thousand_sep'       => ',',           // symbol for thousand separation ',' or ''
 151  
 152    );
 153    var $y_tick_labels     =   null;         // array of text values for y-axis tick labels
 154    var $offset_relation   =   null;         // array of offsets for different sets of data
 155  
 156  
 157      // init all text - title, labels, and axis text.
 158      function init() {
 159  
 160        /// Moodle mods:  overrides the font path and encodings
 161  
 162        global $CFG;
 163  
 164        /// A default.ttf is searched for in this order:
 165        ///      dataroot/lang/xx_local/fonts
 166        ///      dataroot/lang/xx/fonts
 167        ///      dirroot/lang/xx/fonts
 168        ///      dataroot/lang
 169        ///      lib/
 170  
 171        $currlang = current_language();
 172        if (file_exists("$CFG->dataroot/lang/".$currlang."_local/fonts/default.ttf")) {
 173            $fontpath = "$CFG->dataroot/lang/".$currlang."_local/fonts/";
 174        } else if (file_exists("$CFG->dataroot/lang/$currlang/fonts/default.ttf")) {
 175            $fontpath = "$CFG->dataroot/lang/$currlang/fonts/";
 176        } else if (file_exists("$CFG->dirroot/lang/$currlang/fonts/default.ttf")) {
 177            $fontpath = "$CFG->dirroot/lang/$currlang/fonts/";
 178        } else if (file_exists("$CFG->dataroot/lang/default.ttf")) {
 179            $fontpath = "$CFG->dataroot/lang/";
 180        } else {
 181            $fontpath = "$CFG->libdir/";
 182        }
 183  
 184        $this->parameter['path_to_fonts'] = $fontpath;
 185  
 186        /// End Moodle mods
 187  
 188  
 189  
 190        $this->calculated['outer_border'] = $this->calculated['boundary_box'];
 191  
 192        // outer padding
 193        $this->calculated['boundary_box']['left']   += $this->parameter['outer_padding'];
 194        $this->calculated['boundary_box']['top']    += $this->parameter['outer_padding'];
 195        $this->calculated['boundary_box']['right']  -= $this->parameter['outer_padding'];
 196        $this->calculated['boundary_box']['bottom'] -= $this->parameter['outer_padding'];
 197  
 198        $this->init_x_axis();
 199        $this->init_y_axis();
 200        $this->init_legend();
 201        $this->init_labels();
 202  
 203        //  take into account tick lengths
 204        $this->calculated['bottom_inner_padding'] = $this->parameter['x_inner_padding'];
 205        if (($this->parameter['x_ticks_colour'] != 'none') && ($this->parameter['tick_length'] < 0))
 206          $this->calculated['bottom_inner_padding'] -= $this->parameter['tick_length'];
 207        $this->calculated['boundary_box']['bottom'] -= $this->calculated['bottom_inner_padding'];
 208  
 209        $this->calculated['left_inner_padding'] = $this->parameter['y_inner_padding'];
 210        if ($this->parameter['y_axis_text_left']) {
 211          if (($this->parameter['y_ticks_colour'] != 'none') && ($this->parameter['tick_length'] < 0))
 212            $this->calculated['left_inner_padding'] -= $this->parameter['tick_length'];
 213        }
 214        $this->calculated['boundary_box']['left'] += $this->calculated['left_inner_padding'];
 215  
 216        $this->calculated['right_inner_padding'] = $this->parameter['y_inner_padding'];
 217        if ($this->parameter['y_axis_text_right']) {
 218          if (($this->parameter['y_ticks_colour'] != 'none') && ($this->parameter['tick_length'] < 0))
 219            $this->calculated['right_inner_padding'] -= $this->parameter['tick_length'];
 220        }
 221        $this->calculated['boundary_box']['right'] -= $this->calculated['right_inner_padding'];
 222  
 223        // boundaryBox now has coords for plotting area.
 224        $this->calculated['inner_border'] = $this->calculated['boundary_box'];
 225  
 226        $this->init_data();
 227        $this->init_x_ticks();
 228        $this->init_y_ticks();
 229      }
 230  
 231      function draw_text() {
 232        $colour = $this->parameter['outer_background'];
 233        if ($colour != 'none') $this->draw_rectangle($this->calculated['outer_border'], $colour, 'fill'); // graph background
 234  
 235        // draw border around image
 236        $colour = $this->parameter['outer_border'];
 237        if ($colour != 'none') $this->draw_rectangle($this->calculated['outer_border'], $colour, 'box'); // graph border
 238  
 239        $this->draw_title();
 240        $this->draw_x_label();
 241        $this->draw_y_label_left();
 242        $this->draw_y_label_right();
 243        $this->draw_x_axis();
 244        $this->draw_y_axis();
 245        if      ($this->calculated['y_axis_left']['has_data'])  $this->draw_zero_axis_left();  // either draw zero axis on left
 246        else if ($this->calculated['y_axis_right']['has_data']) $this->draw_zero_axis_right(); // ... or right.
 247        $this->draw_legend();
 248  
 249        // draw border around plot area
 250        $colour = $this->parameter['inner_background'];
 251        if ($colour != 'none') $this->draw_rectangle($this->calculated['inner_border'], $colour, 'fill'); // graph background
 252  
 253        // draw border around image
 254        $colour = $this->parameter['inner_border'];
 255        if ($colour != 'none') $this->draw_rectangle($this->calculated['inner_border'], $colour, $this->parameter['inner_border_type']); // graph border
 256      }
 257  
 258      function draw_stack() {
 259        $this->init();
 260        $this->draw_text();
 261  
 262        $yOrder = $this->y_order; // save y_order data.
 263        // iterate over each data set. order is very important if you want to see data correctly. remember shadows!!
 264        foreach ($yOrder as $set) {
 265          $this->y_order = array($set);
 266          $this->init_data();
 267          $this->draw_data();
 268        }
 269        $this->y_order = $yOrder; // revert y_order data.
 270  
 271        $this->output();
 272      }
 273  
 274      function draw() {
 275        $this->init();
 276        $this->draw_text();
 277        $this->draw_data();
 278        $this->output();
 279      }
 280  
 281      // draw a data set
 282      function draw_set($order, $set, $offset) {
 283        if ($offset) @$this->init_variable($colour, $this->y_format[$set]['shadow'], $this->parameter['shadow']);
 284        else $colour  = $this->y_format[$set]['colour'];
 285        @$this->init_variable($point,      $this->y_format[$set]['point'],      'none');
 286        @$this->init_variable($pointSize,  $this->y_format[$set]['point_size'],  $this->parameter['point_size']);
 287        @$this->init_variable($line,       $this->y_format[$set]['line'],       'none');
 288        @$this->init_variable($brushType,  $this->y_format[$set]['brush_type'],  $this->parameter['brush_type']);
 289        @$this->init_variable($brushSize,  $this->y_format[$set]['brush_size'],  $this->parameter['brush_size']);
 290        @$this->init_variable($bar,        $this->y_format[$set]['bar'],        'none');
 291        @$this->init_variable($barSize,    $this->y_format[$set]['bar_size'],    $this->parameter['bar_size']);
 292        @$this->init_variable($area,       $this->y_format[$set]['area'],       'none');
 293  
 294        $lastX = 0;
 295        $lastY = 'none';
 296        $fromX = 0;
 297        $fromY = 'none';
 298  
 299        //print "set $set<br />";
 300        //expand_pre($this->calculated['y_plot']);
 301  
 302        foreach ($this->x_data as $index => $x) {
 303          //print "index $index<br />";
 304          $thisY = $this->calculated['y_plot'][$set][$index];
 305          $thisX = $this->calculated['x_plot'][$index];
 306  
 307          //print "$thisX, $thisY <br />";
 308  
 309          if (($bar!='none') && (string)$thisY != 'none') {
 310              if (isset($this->offset_relation[$set]) && $relatedset = $this->offset_relation[$set]) {
 311                  $yoffset = $this->calculated['y_plot'][$relatedset][$index];                // Moodle
 312              } else {                                                                        // Moodle
 313                  $yoffset = 0;                                                               // Moodle
 314              }                                                                               // Moodle
 315              //$this->bar($thisX, $thisY, $bar, $barSize, $colour, $offset, $set);           // Moodle
 316              $this->bar($thisX, $thisY, $bar, $barSize, $colour, $offset, $set, $yoffset);   // Moodle
 317          }
 318  
 319          if (($area!='none') && (((string)$lastY != 'none') && ((string)$thisY != 'none')))
 320            $this->area($lastX, $lastY, $thisX, $thisY, $area, $colour, $offset);
 321  
 322          if (($point!='none') && (string)$thisY != 'none') $this->plot($thisX, $thisY, $point, $pointSize, $colour, $offset);
 323  
 324          if (($line!='none') && ((string)$thisY != 'none')) {
 325            if ((string)$fromY != 'none')
 326              $this->line($fromX, $fromY, $thisX, $thisY, $line, $brushType, $brushSize, $colour, $offset);
 327  
 328            $fromY = $thisY; // start next line from here
 329            $fromX = $thisX; // ...
 330          } else {
 331            $fromY = 'none';
 332            $fromX = 'none';
 333          }
 334  
 335          $lastX = $thisX;
 336          $lastY = $thisY;
 337        }
 338      }
 339  
 340      function draw_data() {
 341        // cycle thru y data to be plotted
 342        // first check for drop shadows...
 343        foreach ($this->y_order as $order => $set) {
 344          @$this->init_variable($offset, $this->y_format[$set]['shadow_offset'], $this->parameter['shadow_offset']);
 345          @$this->init_variable($colour, $this->y_format[$set]['shadow'], $this->parameter['shadow']);
 346          if ($colour != 'none') $this->draw_set($order, $set, $offset);
 347  
 348        }
 349  
 350        // then draw data
 351        foreach ($this->y_order as $order => $set) {
 352          $this->draw_set($order, $set, 0);
 353        }
 354      }
 355  
 356      function draw_legend() {
 357        $position      = $this->parameter['legend'];
 358        if ($position == 'none') return; // abort if no border
 359  
 360        $borderColour  = $this->parameter['legend_border'];
 361        $offset        = $this->parameter['legend_offset'];
 362        $padding       = $this->parameter['legend_padding'];
 363        $height        = $this->calculated['legend']['boundary_box_all']['height'];
 364        $width         = $this->calculated['legend']['boundary_box_all']['width'];
 365        $graphTop      = $this->calculated['boundary_box']['top'];
 366        $graphBottom   = $this->calculated['boundary_box']['bottom'];
 367        $graphLeft     = $this->calculated['boundary_box']['left'];
 368        $graphRight    = $this->calculated['boundary_box']['right'];
 369        $outsideRight  = $this->calculated['outer_border']['right'];
 370        $outsideBottom = $this->calculated['outer_border']['bottom'];
 371        switch ($position) {
 372          case 'top-left':
 373            $top    = $graphTop  + $offset;
 374            $bottom = $graphTop  + $height + $offset;
 375            $left   = $graphLeft + $offset;
 376            $right  = $graphLeft + $width + $offset;
 377  
 378            break;
 379          case 'top-right':
 380            $top    = $graphTop   + $offset;
 381            $bottom = $graphTop   + $height + $offset;
 382            $left   = $graphRight - $width - $offset;
 383            $right  = $graphRight - $offset;
 384  
 385            break;
 386          case 'bottom-left':
 387            $top    = $graphBottom - $height - $offset;
 388            $bottom = $graphBottom - $offset;
 389            $left   = $graphLeft   + $offset;
 390            $right  = $graphLeft   + $width + $offset;
 391  
 392            break;
 393          case 'bottom-right':
 394            $top    = $graphBottom - $height - $offset;
 395            $bottom = $graphBottom - $offset;
 396            $left   = $graphRight  - $width - $offset;
 397            $right  = $graphRight  - $offset;
 398            break;
 399  
 400          case 'outside-top' :
 401            $top    = $graphTop;
 402            $bottom = $graphTop     + $height;
 403            $left   = $outsideRight - $width - $offset;
 404            $right  = $outsideRight - $offset;
 405            break;
 406  
 407          case 'outside-bottom' :
 408            $top    = $graphBottom  - $height;
 409            $bottom = $graphBottom;
 410            $left   = $outsideRight - $width - $offset;
 411            $right  = $outsideRight - $offset;
 412           break;
 413  
 414          case 'outside-left' :
 415            $top    = $outsideBottom - $height - $offset;
 416            $bottom = $outsideBottom - $offset;
 417            $left   = $graphLeft;
 418            $right  = $graphLeft     + $width;
 419           break;
 420  
 421          case 'outside-right' :
 422            $top    = $outsideBottom - $height - $offset;
 423            $bottom = $outsideBottom - $offset;
 424            $left   = $graphRight    - $width;
 425            $right  = $graphRight;
 426            break;
 427          default: // default is top left. no particular reason.
 428            $top    = $this->calculated['boundary_box']['top'];
 429            $bottom = $this->calculated['boundary_box']['top'] + $this->calculated['legend']['boundary_box_all']['height'];
 430            $left   = $this->calculated['boundary_box']['left'];
 431            $right  = $this->calculated['boundary_box']['right'] + $this->calculated['legend']['boundary_box_all']['width'];
 432  
 433      }
 434        // legend border
 435        if($borderColour!='none') $this->draw_rectangle(array('top' => $top,
 436                                                              'left' => $left,
 437                                                              'bottom' => $bottom,
 438                                                              'right' => $right), $this->parameter['legend_border'], 'box');
 439  
 440        // legend text
 441        $legendText = array('points' => $this->parameter['legend_size'],
 442                            'angle'  => 0,
 443                            'font'   => $this->parameter['legend_font'],
 444                            'colour' => $this->parameter['legend_colour']);
 445  
 446        $box = $this->calculated['legend']['boundary_box_max']['height']; // use max height for legend square size.
 447        $x = $left + $padding;
 448        $x_text = $x + $box * 2;
 449        $y = $top + $padding;
 450  
 451        foreach ($this->y_order as $set) {
 452          $legendText['text'] = $this->calculated['legend']['text'][$set];
 453          if ($legendText['text'] != 'none') {
 454            // if text exists then draw box and text
 455            $boxColour = $this->colour[$this->y_format[$set]['colour']];
 456  
 457            // draw box
 458            ImageFilledRectangle($this->image, $x, $y, $x + $box, $y + $box, $boxColour);
 459  
 460            // draw text
 461            $coords = array('x' => $x + $box * 2, 'y' => $y, 'reference' => 'top-left');
 462            $legendText['boundary_box'] = $this->calculated['legend']['boundary_box'][$set];
 463            $this->update_boundaryBox($legendText['boundary_box'], $coords);
 464            $this->print_TTF($legendText);
 465            $y += $padding + $box;
 466          }
 467        }
 468  
 469      }
 470  
 471      function draw_y_label_right() {
 472        if (!$this->parameter['y_label_right']) return;
 473        $x = $this->calculated['boundary_box']['right'] + $this->parameter['y_inner_padding'];
 474        if ($this->parameter['y_axis_text_right']) $x += $this->calculated['y_axis_right']['boundary_box_max']['width']
 475                                                 + $this->calculated['right_inner_padding'];
 476        $y = ($this->calculated['boundary_box']['bottom'] + $this->calculated['boundary_box']['top']) / 2;
 477  
 478        $label = $this->calculated['y_label_right'];
 479        $coords = array('x' => $x, 'y' => $y, 'reference' => 'left-center');
 480        $this->update_boundaryBox($label['boundary_box'], $coords);
 481        $this->print_TTF($label);
 482      }
 483  
 484  
 485      function draw_y_label_left() {
 486        if (!$this->parameter['y_label_left']) return;
 487        $x = $this->calculated['boundary_box']['left'] - $this->parameter['y_inner_padding'];
 488        if ($this->parameter['y_axis_text_left']) $x -= $this->calculated['y_axis_left']['boundary_box_max']['width']
 489                                                 + $this->calculated['left_inner_padding'];
 490        $y = ($this->calculated['boundary_box']['bottom'] + $this->calculated['boundary_box']['top']) / 2;
 491  
 492        $label = $this->calculated['y_label_left'];
 493        $coords = array('x' => $x, 'y' => $y, 'reference' => 'right-center');
 494        $this->update_boundaryBox($label['boundary_box'], $coords);
 495        $this->print_TTF($label);
 496      }
 497  
 498      function draw_title() {
 499        if (!$this->parameter['title']) return;
 500        //$y = $this->calculated['outside_border']['top'] + $this->parameter['outer_padding'];
 501        $y = $this->calculated['boundary_box']['top'] - $this->parameter['outer_padding'];
 502        $x = ($this->calculated['boundary_box']['right'] + $this->calculated['boundary_box']['left']) / 2;
 503        $label = $this->calculated['title'];
 504        $coords = array('x' => $x, 'y' => $y, 'reference' => 'bottom-center');
 505        $this->update_boundaryBox($label['boundary_box'], $coords);
 506        $this->print_TTF($label);
 507      }
 508  
 509      function draw_x_label() {
 510        if (!$this->parameter['x_label']) return;
 511        $y = $this->calculated['boundary_box']['bottom'] + $this->parameter['x_inner_padding'];
 512        if ($this->parameter['x_axis_text']) $y += $this->calculated['x_axis']['boundary_box_max']['height']
 513                                                + $this->calculated['bottom_inner_padding'];
 514        $x = ($this->calculated['boundary_box']['right'] + $this->calculated['boundary_box']['left']) / 2;
 515        $label = $this->calculated['x_label'];
 516        $coords = array('x' => $x, 'y' => $y, 'reference' => 'top-center');
 517        $this->update_boundaryBox($label['boundary_box'], $coords);
 518        $this->print_TTF($label);
 519      }
 520  
 521      function draw_zero_axis_left() {
 522        $colour = $this->parameter['zero_axis'];
 523        if ($colour == 'none') return;
 524        // draw zero axis on left hand side
 525        $this->calculated['zero_axis'] = round($this->calculated['boundary_box']['top']  + ($this->calculated['y_axis_left']['max'] * $this->calculated['y_axis_left']['factor']));
 526        ImageLine($this->image, $this->calculated['boundary_box']['left'], $this->calculated['zero_axis'], $this->calculated['boundary_box']['right'], $this->calculated['zero_axis'], $this->colour[$colour]);
 527      }
 528  
 529      function draw_zero_axis_right() {
 530        $colour = $this->parameter['zero_axis'];
 531        if ($colour == 'none') return;
 532        // draw zero axis on right hand side
 533        $this->calculated['zero_axis'] = round($this->calculated['boundary_box']['top']  + ($this->calculated['y_axis_right']['max'] * $this->calculated['y_axis_right']['factor']));
 534        ImageLine($this->image, $this->calculated['boundary_box']['left'], $this->calculated['zero_axis'], $this->calculated['boundary_box']['right'], $this->calculated['zero_axis'], $this->colour[$colour]);
 535      }
 536  
 537      function draw_x_axis() {
 538        $gridColour  = $this->colour[$this->parameter['grid_colour']];
 539        $tickColour  = $this->colour[$this->parameter['x_ticks_colour']];
 540        $axis_colour  = $this->parameter['axis_colour'];
 541        $xGrid       = $this->parameter['x_grid'];
 542        $gridTop     = $this->calculated['boundary_box']['top'];
 543        $gridBottom  = $this->calculated['boundary_box']['bottom'];
 544  
 545        if ($this->parameter['tick_length'] >= 0) {
 546          $tickTop     = $this->calculated['boundary_box']['bottom'] - $this->parameter['tick_length'];
 547          $tickBottom  = $this->calculated['boundary_box']['bottom'];
 548          $textBottom  = $tickBottom + $this->calculated['bottom_inner_padding'];
 549        } else {
 550          $tickTop     = $this->calculated['boundary_box']['bottom'];
 551          $tickBottom  = $this->calculated['boundary_box']['bottom'] - $this->parameter['tick_length'];
 552          $textBottom  = $tickBottom + $this->calculated['bottom_inner_padding'];
 553        }
 554  
 555        $axis_font    = $this->parameter['axis_font'];
 556        $axis_size    = $this->parameter['axis_size'];
 557        $axis_angle   = $this->parameter['x_axis_angle'];
 558  
 559        if ($axis_angle == 0)  $reference = 'top-center';
 560        if ($axis_angle > 0)   $reference = 'top-right';
 561        if ($axis_angle < 0)   $reference = 'top-left';
 562        if ($axis_angle == 90) $reference = 'top-center';
 563  
 564        //generic tag information. applies to all axis text.
 565        $axisTag = array('points' => $axis_size, 'angle' => $axis_angle, 'font' => $axis_font, 'colour' => $axis_colour);
 566  
 567        foreach ($this->calculated['x_axis']['tick_x'] as $set => $tickX) {
 568          // draw x grid if colour specified
 569          if ($xGrid != 'none') {
 570            switch ($xGrid) {
 571              case 'line':
 572                ImageLine($this->image, round($tickX), round($gridTop), round($tickX), round($gridBottom), $gridColour);
 573                break;
 574               case 'dash':
 575                ImageDashedLine($this->image, round($tickX), round($gridTop), round($tickX), round($gridBottom), $gridColour);
 576                break;
 577            }
 578          }
 579  
 580          if ($this->parameter['x_axis_text'] && !($set % $this->parameter['x_axis_text'])) { // test if tick should be displayed
 581            // draw tick
 582            if ($tickColour != 'none')
 583              ImageLine($this->image, round($tickX), round($tickTop), round($tickX), round($tickBottom), $tickColour);
 584  
 585            // draw axis text
 586            $coords = array('x' => $tickX, 'y' => $textBottom, 'reference' => $reference);
 587            $axisTag['text'] = $this->calculated['x_axis']['text'][$set];
 588            $axisTag['boundary_box'] = $this->calculated['x_axis']['boundary_box'][$set];
 589            $this->update_boundaryBox($axisTag['boundary_box'], $coords);
 590            $this->print_TTF($axisTag);
 591          }
 592        }
 593      }
 594  
 595      function draw_y_axis() {
 596        $gridColour  = $this->colour[$this->parameter['grid_colour']];
 597        $tickColour  = $this->colour[$this->parameter['y_ticks_colour']];
 598        $axis_colour  = $this->parameter['axis_colour'];
 599        $yGrid       = $this->parameter['y_grid'];
 600        $gridLeft    = $this->calculated['boundary_box']['left'];
 601        $gridRight   = $this->calculated['boundary_box']['right'];
 602  
 603        // axis font information
 604        $axis_font    = $this->parameter['axis_font'];
 605        $axis_size    = $this->parameter['axis_size'];
 606        $axis_angle   = $this->parameter['y_axis_angle'];
 607        $axisTag = array('points' => $axis_size, 'angle' => $axis_angle, 'font' => $axis_font, 'colour' => $axis_colour);
 608  
 609  
 610        if ($this->calculated['y_axis_left']['has_data']) {
 611          // LEFT HAND SIDE
 612          // left and right coords for ticks
 613          if ($this->parameter['tick_length'] >= 0) {
 614            $tickLeft     = $this->calculated['boundary_box']['left'];
 615            $tickRight    = $this->calculated['boundary_box']['left'] + $this->parameter['tick_length'];
 616          } else {
 617            $tickLeft     = $this->calculated['boundary_box']['left'] + $this->parameter['tick_length'];
 618            $tickRight    = $this->calculated['boundary_box']['left'];
 619          }
 620          $textRight      = $tickLeft - $this->calculated['left_inner_padding'];
 621  
 622          if ($axis_angle == 0)  $reference = 'right-center';
 623          if ($axis_angle > 0)   $reference = 'right-top';
 624          if ($axis_angle < 0)   $reference = 'right-bottom';
 625          if ($axis_angle == 90) $reference = 'right-center';
 626  
 627          foreach ($this->calculated['y_axis']['tick_y'] as $set => $tickY) {
 628            // draw y grid if colour specified
 629            if ($yGrid != 'none') {
 630              switch ($yGrid) {
 631                case 'line':
 632                  ImageLine($this->image, round($gridLeft), round($tickY), round($gridRight), round($tickY), $gridColour);
 633                  break;
 634                 case 'dash':
 635                  ImageDashedLine($this->image, round($gridLeft), round($tickY), round($gridRight), round($tickY), $gridColour);
 636                  break;
 637              }
 638            }
 639  
 640            // y axis text
 641            if ($this->parameter['y_axis_text_left'] && !($set % $this->parameter['y_axis_text_left'])) { // test if tick should be displayed
 642              // draw tick
 643              if ($tickColour != 'none')
 644                ImageLine($this->image, round($tickLeft), round($tickY), round($tickRight), round($tickY), $tickColour);
 645  
 646              // draw axis text...
 647              $coords = array('x' => $textRight, 'y' => $tickY, 'reference' => $reference);
 648              $axisTag['text'] = $this->calculated['y_axis_left']['text'][$set];
 649              $axisTag['boundary_box'] = $this->calculated['y_axis_left']['boundary_box'][$set];
 650              $this->update_boundaryBox($axisTag['boundary_box'], $coords);
 651              $this->print_TTF($axisTag);
 652            }
 653          }
 654        }
 655  
 656        if ($this->calculated['y_axis_right']['has_data']) {
 657          // RIGHT HAND SIDE
 658          // left and right coords for ticks
 659          if ($this->parameter['tick_length'] >= 0) {
 660            $tickLeft     = $this->calculated['boundary_box']['right'] - $this->parameter['tick_length'];
 661            $tickRight    = $this->calculated['boundary_box']['right'];
 662          } else {
 663            $tickLeft     = $this->calculated['boundary_box']['right'];
 664            $tickRight    = $this->calculated['boundary_box']['right'] - $this->parameter['tick_length'];
 665          }
 666          $textLeft       = $tickRight+ $this->calculated['left_inner_padding'];
 667  
 668          if ($axis_angle == 0)  $reference = 'left-center';
 669          if ($axis_angle > 0)   $reference = 'left-bottom';
 670          if ($axis_angle < 0)   $reference = 'left-top';
 671          if ($axis_angle == 90) $reference = 'left-center';
 672  
 673          foreach ($this->calculated['y_axis']['tick_y'] as $set => $tickY) {
 674            if (!$this->calculated['y_axis_left']['has_data'] && $yGrid != 'none') { // draw grid if not drawn already (above)
 675              switch ($yGrid) {
 676                case 'line':
 677                  ImageLine($this->image, round($gridLeft), round($tickY), round($gridRight), round($tickY), $gridColour);
 678                  break;
 679                 case 'dash':
 680                  ImageDashedLine($this->image, round($gridLeft), round($tickY), round($gridRight), round($tickY), $gridColour);
 681                  break;
 682              }
 683            }
 684  
 685            if ($this->parameter['y_axis_text_right'] && !($set % $this->parameter['y_axis_text_right'])) { // test if tick should be displayed
 686              // draw tick
 687              if ($tickColour != 'none')
 688                ImageLine($this->image, round($tickLeft), round($tickY), round($tickRight), round($tickY), $tickColour);
 689  
 690              // draw axis text...
 691              $coords = array('x' => $textLeft, 'y' => $tickY, 'reference' => $reference);
 692              $axisTag['text'] = $this->calculated['y_axis_right']['text'][$set];
 693              $axisTag['boundary_box'] = $this->calculated['y_axis_left']['boundary_box'][$set];
 694              $this->update_boundaryBox($axisTag['boundary_box'], $coords);
 695              $this->print_TTF($axisTag);
 696            }
 697          }
 698        }
 699      }
 700  
 701      function init_data() {
 702        $this->calculated['y_plot'] = array(); // array to hold pixel plotting coords for y axis
 703        $height = $this->calculated['boundary_box']['bottom'] - $this->calculated['boundary_box']['top'];
 704        $width  = $this->calculated['boundary_box']['right'] - $this->calculated['boundary_box']['left'];
 705  
 706        // calculate pixel steps between axis ticks.
 707        $this->calculated['y_axis']['step'] = $height / ($this->parameter['y_axis_gridlines'] - 1);
 708  
 709        // calculate x ticks spacing taking into account x offset for ticks.
 710        $extraTick  = 2 * $this->parameter['x_offset']; // extra tick to account for padding
 711        $numTicks = $this->calculated['x_axis']['num_ticks'] - 1;    // number of x ticks
 712  
 713        // Hack by rodger to avoid division by zero, see bug 1231
 714        if ($numTicks==0) $numTicks=1;
 715  
 716        $this->calculated['x_axis']['step'] = $width / ($numTicks + $extraTick);
 717        $widthPlot = $width - ($this->calculated['x_axis']['step'] * $extraTick);
 718        $this->calculated['x_axis']['step'] = $widthPlot / $numTicks;
 719  
 720        //calculate factor for transforming x,y physical coords to logical coords for right hand y_axis.
 721        $y_range = $this->calculated['y_axis_right']['max'] - $this->calculated['y_axis_right']['min'];
 722        $y_range = ($y_range ? $y_range : 1);
 723        $this->calculated['y_axis_right']['factor'] = $height / $y_range;
 724  
 725        //calculate factor for transforming x,y physical coords to logical coords for left hand axis.
 726        $yRange = $this->calculated['y_axis_left']['max'] - $this->calculated['y_axis_left']['min'];
 727        $yRange = ($yRange ? $yRange : 1);
 728        $this->calculated['y_axis_left']['factor'] = $height / $yRange;
 729        if ($this->parameter['x_axis_gridlines'] != 'auto') {
 730          $xRange = $this->calculated['x_axis']['max'] - $this->calculated['x_axis']['min'];
 731          $xRange = ($xRange ? $xRange : 1);
 732          $this->calculated['x_axis']['factor'] = $widthPlot / $xRange;
 733        }
 734  
 735        //expand_pre($this->calculated['boundary_box']);
 736        // cycle thru all data sets...
 737        $this->calculated['num_bars'] = 0;
 738        foreach ($this->y_order as $order => $set) {
 739          // determine how many bars there are
 740          if (isset($this->y_format[$set]['bar']) && ($this->y_format[$set]['bar'] != 'none')) {
 741            $this->calculated['bar_offset_index'][$set] = $this->calculated['num_bars']; // index to relate bar with data set.
 742            $this->calculated['num_bars']++;
 743          }
 744  
 745          // calculate y coords for plotting data
 746          foreach ($this->x_data as $index => $x) {
 747            $this->calculated['y_plot'][$set][$index] = $this->y_data[$set][$index];
 748  
 749            if ((string)$this->y_data[$set][$index] != 'none') {
 750  
 751              if (isset($this->y_format[$set]['y_axis']) && $this->y_format[$set]['y_axis'] == 'right') {
 752                $this->calculated['y_plot'][$set][$index] =
 753                  round(($this->y_data[$set][$index] - $this->calculated['y_axis_right']['min'])
 754                    * $this->calculated['y_axis_right']['factor']);
 755              } else {
 756                //print "$set $index<br />";
 757                $this->calculated['y_plot'][$set][$index] =
 758                  round(($this->y_data[$set][$index] - $this->calculated['y_axis_left']['min'])
 759                    * $this->calculated['y_axis_left']['factor']);
 760              }
 761  
 762            }
 763          }
 764        }
 765        //print "factor ".$this->calculated['x_axis']['factor']."<br />";
 766        //expand_pre($this->calculated['x_plot']);
 767  
 768        // calculate bar parameters if bars are to be drawn.
 769        if ($this->calculated['num_bars']) {
 770          $xStep       = $this->calculated['x_axis']['step'];
 771          $totalWidth  = $this->calculated['x_axis']['step'] - $this->parameter['bar_spacing'];
 772          $barWidth    = $totalWidth / $this->calculated['num_bars'];
 773  
 774          $barX = ($barWidth - $totalWidth) / 2; // starting x offset
 775          for ($i=0; $i < $this->calculated['num_bars']; $i++) {
 776            $this->calculated['bar_offset_x'][$i] = $barX;
 777            $barX += $barWidth; // add width of bar to x offset.
 778          }
 779          $this->calculated['bar_width'] = $barWidth;
 780        }
 781  
 782  
 783      }
 784  
 785      function init_x_ticks() {
 786        // get coords for x axis ticks and data plots
 787        //$xGrid       = $this->parameter['x_grid'];
 788        $xStep       = $this->calculated['x_axis']['step'];
 789        $ticksOffset = $this->parameter['x_offset']; // where to start drawing ticks relative to y axis.
 790        $gridLeft    = $this->calculated['boundary_box']['left'] + ($xStep * $ticksOffset); // grid x start
 791        $tickX       = $gridLeft; // tick x coord
 792  
 793        foreach ($this->calculated['x_axis']['text'] as $set => $value) {
 794          //print "index: $set<br />";
 795          // x tick value
 796          $this->calculated['x_axis']['tick_x'][$set] = $tickX;
 797          // if num ticks is auto then x plot value is same as x  tick
 798          if ($this->parameter['x_axis_gridlines'] == 'auto') $this->calculated['x_plot'][$set] = round($tickX);
 799          //print $this->calculated['x_plot'][$set].'<br />';
 800          $tickX += $xStep;
 801        }
 802  
 803        //print "xStep: $xStep <br />";
 804        // if numeric x axis then calculate x coords for each data point. this is seperate from x ticks.
 805        $gridX = $gridLeft;
 806        if (empty($this->calculated['x_axis']['factor'])) {
 807            $this->calculated['x_axis']['factor'] = 0;
 808        }
 809        if (empty($this->calculated['x_axis']['min'])) {
 810            $this->calculated['x_axis']['min'] = 0;
 811        }
 812        $factor = $this->calculated['x_axis']['factor'];
 813        $min = $this->calculated['x_axis']['min'];
 814  
 815        if ($this->parameter['x_axis_gridlines'] != 'auto') {
 816          foreach ($this->x_data as $index => $x) {
 817            //print "index: $index, x: $x<br />";
 818            $offset = $x - $this->calculated['x_axis']['min'];
 819  
 820            //$gridX = ($offset * $this->calculated['x_axis']['factor']);
 821            //print "offset: $offset <br />";
 822            //$this->calculated['x_plot'][$set] = $gridLeft + ($offset * $this->calculated['x_axis']['factor']);
 823  
 824            $this->calculated['x_plot'][$index] = $gridLeft + ($x - $min) * $factor;
 825  
 826            //print $this->calculated['x_plot'][$set].'<br />';
 827          }
 828        }
 829        //expand_pre($this->calculated['boundary_box']);
 830        //print "factor ".$this->calculated['x_axis']['factor']."<br />";
 831        //expand_pre($this->calculated['x_plot']);
 832      }
 833  
 834      function init_y_ticks() {
 835        // get coords for y axis ticks
 836  
 837        $yStep      = $this->calculated['y_axis']['step'];
 838        $gridBottom = $this->calculated['boundary_box']['bottom'];
 839        $tickY      = $gridBottom; // tick y coord
 840  
 841        for ($i = 0; $i < $this->parameter['y_axis_gridlines']; $i++) {
 842          $this->calculated['y_axis']['tick_y'][$i] = $tickY;
 843          $tickY   -= $yStep;
 844        }
 845  
 846      }
 847  
 848      function init_labels() {
 849        if ($this->parameter['title']) {
 850          $size = $this->get_boundaryBox(
 851            array('points' => $this->parameter['title_size'],
 852                  'angle'  => 0,
 853                  'font'   => $this->parameter['title_font'],
 854                  'text'   => $this->parameter['title']));
 855          $this->calculated['title']['boundary_box']  = $size;
 856          $this->calculated['title']['text']         = $this->parameter['title'];
 857          $this->calculated['title']['font']         = $this->parameter['title_font'];
 858          $this->calculated['title']['points']       = $this->parameter['title_size'];
 859          $this->calculated['title']['colour']       = $this->parameter['title_colour'];
 860          $this->calculated['title']['angle']        = 0;
 861  
 862          $this->calculated['boundary_box']['top'] += $size['height'] + $this->parameter['outer_padding'];
 863          //$this->calculated['boundary_box']['top'] += $size['height'];
 864  
 865        } else $this->calculated['title']['boundary_box'] = $this->get_null_size();
 866  
 867        if ($this->parameter['y_label_left']) {
 868          $this->calculated['y_label_left']['text']    = $this->parameter['y_label_left'];
 869          $this->calculated['y_label_left']['angle']   = $this->parameter['y_label_angle'];
 870          $this->calculated['y_label_left']['font']    = $this->parameter['label_font'];
 871          $this->calculated['y_label_left']['points']  = $this->parameter['label_size'];
 872          $this->calculated['y_label_left']['colour']  = $this->parameter['label_colour'];
 873  
 874          $size = $this->get_boundaryBox($this->calculated['y_label_left']);
 875          $this->calculated['y_label_left']['boundary_box']  = $size;
 876          //$this->calculated['boundary_box']['left'] += $size['width'] + $this->parameter['inner_padding'];
 877          $this->calculated['boundary_box']['left'] += $size['width'];
 878  
 879        } else $this->calculated['y_label_left']['boundary_box'] = $this->get_null_size();
 880  
 881        if ($this->parameter['y_label_right']) {
 882          $this->calculated['y_label_right']['text']    = $this->parameter['y_label_right'];
 883          $this->calculated['y_label_right']['angle']   = $this->parameter['y_label_angle'];
 884          $this->calculated['y_label_right']['font']    = $this->parameter['label_font'];
 885          $this->calculated['y_label_right']['points']  = $this->parameter['label_size'];
 886          $this->calculated['y_label_right']['colour']  = $this->parameter['label_colour'];
 887  
 888          $size = $this->get_boundaryBox($this->calculated['y_label_right']);
 889          $this->calculated['y_label_right']['boundary_box']  = $size;
 890          //$this->calculated['boundary_box']['right'] -= $size['width'] + $this->parameter['inner_padding'];
 891          $this->calculated['boundary_box']['right'] -= $size['width'];
 892  
 893        } else $this->calculated['y_label_right']['boundary_box'] = $this->get_null_size();
 894  
 895        if ($this->parameter['x_label']) {
 896          $this->calculated['x_label']['text']         = $this->parameter['x_label'];
 897          $this->calculated['x_label']['angle']        = $this->parameter['x_label_angle'];
 898          $this->calculated['x_label']['font']         = $this->parameter['label_font'];
 899          $this->calculated['x_label']['points']       = $this->parameter['label_size'];
 900          $this->calculated['x_label']['colour']       = $this->parameter['label_colour'];
 901  
 902          $size = $this->get_boundaryBox($this->calculated['x_label']);
 903          $this->calculated['x_label']['boundary_box']  = $size;
 904          //$this->calculated['boundary_box']['bottom'] -= $size['height'] + $this->parameter['inner_padding'];
 905          $this->calculated['boundary_box']['bottom'] -= $size['height'];
 906  
 907        } else $this->calculated['x_label']['boundary_box'] = $this->get_null_size();
 908  
 909      }
 910  
 911  
 912      function init_legend() {
 913        $this->calculated['legend'] = array(); // array to hold calculated values for legend.
 914        //$this->calculated['legend']['boundary_box_max'] = array('height' => 0, 'width' => 0);
 915        $this->calculated['legend']['boundary_box_max'] = $this->get_null_size();
 916        if ($this->parameter['legend'] == 'none') return;
 917  
 918        $position = $this->parameter['legend'];
 919        $numSets = 0; // number of data sets with legends.
 920        $sumTextHeight = 0; // total of height of all legend text items.
 921        $width = 0;
 922        $height = 0;
 923  
 924        foreach ($this->y_order as $set) {
 925         $text = isset($this->y_format[$set]['legend']) ? $this->y_format[$set]['legend'] : 'none';
 926         $size = $this->get_boundaryBox(
 927           array('points' => $this->parameter['legend_size'],
 928                 'angle'  => 0,
 929                 'font'   => $this->parameter['legend_font'],
 930                 'text'   => $text));
 931  
 932         $this->calculated['legend']['boundary_box'][$set] = $size;
 933         $this->calculated['legend']['text'][$set]        = $text;
 934         //$this->calculated['legend']['font'][$set]        = $this->parameter['legend_font'];
 935         //$this->calculated['legend']['points'][$set]      = $this->parameter['legend_size'];
 936         //$this->calculated['legend']['angle'][$set]       = 0;
 937  
 938         if ($text && $text!='none') {
 939           $numSets++;
 940           $sumTextHeight += $size['height'];
 941         }
 942  
 943         if ($size['width'] > $this->calculated['legend']['boundary_box_max']['width'])
 944           $this->calculated['legend']['boundary_box_max'] = $size;
 945        }
 946  
 947        $offset  = $this->parameter['legend_offset'];  // offset in pixels of legend box from graph border.
 948        $padding = $this->parameter['legend_padding']; // padding in pixels around legend text.
 949        $textWidth = $this->calculated['legend']['boundary_box_max']['width']; // width of largest legend item.
 950        $textHeight = $this->calculated['legend']['boundary_box_max']['height']; // use height as size to use for colour square in legend.
 951        $width = $padding * 2 + $textWidth + $textHeight * 2;  // left and right padding + maximum text width + space for square
 952        $height = ($padding + $textHeight) * $numSets + $padding; // top and bottom padding + padding between text + text.
 953  
 954        $this->calculated['legend']['boundary_box_all'] = array('width'     => $width,
 955                                                              'height'    => $height,
 956                                                              'offset'    => $offset,
 957                                                              'reference' => $position);
 958  
 959        switch ($position) { // move in right or bottom if legend is outside data plotting area.
 960          case 'outside-top' :
 961            $this->calculated['boundary_box']['right']      -= $offset + $width; // move in right hand side
 962            break;
 963  
 964          case 'outside-bottom' :
 965            $this->calculated['boundary_box']['right']      -= $offset + $width; // move in right hand side
 966            break;
 967  
 968          case 'outside-left' :
 969            $this->calculated['boundary_box']['bottom']      -= $offset + $height; // move in right hand side
 970            break;
 971  
 972          case 'outside-right' :
 973            $this->calculated['boundary_box']['bottom']      -= $offset + $height; // move in right hand side
 974            break;
 975        }
 976      }
 977  
 978      function init_y_axis() {
 979        $this->calculated['y_axis_left'] = array(); // array to hold calculated values for y_axis on left.
 980        $this->calculated['y_axis_left']['boundary_box_max'] = $this->get_null_size();
 981        $this->calculated['y_axis_right'] = array(); // array to hold calculated values for y_axis on right.
 982        $this->calculated['y_axis_right']['boundary_box_max'] = $this->get_null_size();
 983  
 984        $axis_font       = $this->parameter['axis_font'];
 985        $axis_size       = $this->parameter['axis_size'];
 986        $axis_colour     = $this->parameter['axis_colour'];
 987        $axis_angle      = $this->parameter['y_axis_angle'];
 988        $y_tick_labels   = $this->y_tick_labels;
 989  
 990        $this->calculated['y_axis_left']['has_data'] = FALSE;
 991        $this->calculated['y_axis_right']['has_data'] = FALSE;
 992  
 993        // find min and max y values.
 994        $minLeft = $this->parameter['y_min_left'];
 995        $maxLeft = $this->parameter['y_max_left'];
 996        $minRight = $this->parameter['y_min_right'];
 997        $maxRight = $this->parameter['y_max_right'];
 998        $dataLeft = array();
 999        $dataRight = array();
1000        foreach ($this->y_order as $order => $set) {
1001          if (isset($this->y_format[$set]['y_axis']) && $this->y_format[$set]['y_axis'] == 'right') {
1002            $this->calculated['y_axis_right']['has_data'] = TRUE;
1003            $dataRight = array_merge($dataRight, $this->y_data[$set]);
1004          } else {
1005            $this->calculated['y_axis_left']['has_data'] = TRUE;
1006            $dataLeft = array_merge($dataLeft, $this->y_data[$set]);
1007          }
1008        }
1009        $dataLeftRange = $this->find_range($dataLeft, $minLeft, $maxLeft, $this->parameter['y_resolution_left']);
1010        $dataRightRange = $this->find_range($dataRight, $minRight, $maxRight, $this->parameter['y_resolution_right']);
1011        $minLeft = $dataLeftRange['min'];
1012        $maxLeft = $dataLeftRange['max'];
1013        $minRight = $dataRightRange['min'];
1014        $maxRight = $dataRightRange['max'];
1015  
1016        $this->calculated['y_axis_left']['min']  = $minLeft;
1017        $this->calculated['y_axis_left']['max']  = $maxLeft;
1018        $this->calculated['y_axis_right']['min'] = $minRight;
1019        $this->calculated['y_axis_right']['max'] = $maxRight;
1020  
1021        $stepLeft = ($maxLeft - $minLeft) / ($this->parameter['y_axis_gridlines'] - 1);
1022        $startLeft = $minLeft;
1023        $step_right = ($maxRight - $minRight) / ($this->parameter['y_axis_gridlines'] - 1);
1024        $start_right = $minRight;
1025  
1026        if ($this->parameter['y_axis_text_left']) {
1027          for ($i = 0; $i < $this->parameter['y_axis_gridlines']; $i++) { // calculate y axis text sizes
1028            // left y axis
1029            if ($y_tick_labels) {
1030              $value = $y_tick_labels[$i];
1031            } else {
1032              $value = number_format($startLeft, $this->parameter['y_decimal_left'], $this->parameter['decimal_point'], $this->parameter['thousand_sep']);
1033            }
1034            $this->calculated['y_axis_left']['data'][$i]  = $startLeft;
1035            $this->calculated['y_axis_left']['text'][$i]  = $value; // text is formatted raw data
1036  
1037            $size = $this->get_boundaryBox(
1038              array('points' => $axis_size,
1039                    'font'   => $axis_font,
1040                    'angle'  => $axis_angle,
1041                    'colour' => $axis_colour,
1042                    'text'   => $value));
1043            $this->calculated['y_axis_left']['boundary_box'][$i] = $size;
1044  
1045            if ($size['height'] > $this->calculated['y_axis_left']['boundary_box_max']['height'])
1046              $this->calculated['y_axis_left']['boundary_box_max']['height'] = $size['height'];
1047            if ($size['width'] > $this->calculated['y_axis_left']['boundary_box_max']['width'])
1048              $this->calculated['y_axis_left']['boundary_box_max']['width'] = $size['width'];
1049  
1050            $startLeft += $stepLeft;
1051          }
1052          $this->calculated['boundary_box']['left'] += $this->calculated['y_axis_left']['boundary_box_max']['width']
1053                                                      + $this->parameter['y_inner_padding'];
1054        }
1055  
1056        if ($this->parameter['y_axis_text_right']) {
1057          for ($i = 0; $i < $this->parameter['y_axis_gridlines']; $i++) { // calculate y axis text sizes
1058            // right y axis
1059            $value = number_format($start_right, $this->parameter['y_decimal_right'], $this->parameter['decimal_point'], $this->parameter['thousand_sep']);
1060            $this->calculated['y_axis_right']['data'][$i]  = $start_right;
1061            $this->calculated['y_axis_right']['text'][$i]  = $value; // text is formatted raw data
1062            $size = $this->get_boundaryBox(
1063              array('points' => $axis_size,
1064                    'font'   => $axis_font,
1065                    'angle'  => $axis_angle,
1066                    'colour' => $axis_colour,
1067                    'text'   => $value));
1068            $this->calculated['y_axis_right']['boundary_box'][$i] = $size;
1069  
1070            if ($size['height'] > $this->calculated['y_axis_right']['boundary_box_max']['height'])
1071              $this->calculated['y_axis_right']['boundary_box_max'] = $size;
1072            if ($size['width'] > $this->calculated['y_axis_right']['boundary_box_max']['width'])
1073              $this->calculated['y_axis_right']['boundary_box_max']['width'] = $size['width'];
1074  
1075            $start_right += $step_right;
1076          }
1077          $this->calculated['boundary_box']['right'] -= $this->calculated['y_axis_right']['boundary_box_max']['width']
1078                                                      + $this->parameter['y_inner_padding'];
1079        }
1080      }
1081  
1082      function init_x_axis() {
1083        $this->calculated['x_axis'] = array(); // array to hold calculated values for x_axis.
1084        $this->calculated['x_axis']['boundary_box_max'] = array('height' => 0, 'width' => 0);
1085  
1086        $axis_font       = $this->parameter['axis_font'];
1087        $axis_size       = $this->parameter['axis_size'];
1088        $axis_colour     = $this->parameter['axis_colour'];
1089        $axis_angle      = $this->parameter['x_axis_angle'];
1090  
1091        // check whether to treat x axis as numeric
1092        if ($this->parameter['x_axis_gridlines'] == 'auto') { // auto means text based x_axis, not numeric...
1093          $this->calculated['x_axis']['num_ticks'] = sizeof($this->x_data);
1094            $data = $this->x_data;
1095            for ($i=0; $i < $this->calculated['x_axis']['num_ticks']; $i++) {
1096              $value = array_shift($data); // grab value from begin of array
1097              $this->calculated['x_axis']['data'][$i]  = $value;
1098              $this->calculated['x_axis']['text'][$i]  = $value; // raw data and text are both the same in this case
1099              $size = $this->get_boundaryBox(
1100                array('points' => $axis_size,
1101                      'font'   => $axis_font,
1102                      'angle'  => $axis_angle,
1103                      'colour' => $axis_colour,
1104                      'text'   => $value));
1105              $this->calculated['x_axis']['boundary_box'][$i] = $size;
1106              if ($size['height'] > $this->calculated['x_axis']['boundary_box_max']['height'])
1107                $this->calculated['x_axis']['boundary_box_max'] = $size;
1108            }
1109  
1110        } else { // x axis is numeric so find max min values...
1111          $this->calculated['x_axis']['num_ticks'] = $this->parameter['x_axis_gridlines'];
1112  
1113          $min = $this->parameter['x_min'];
1114          $max = $this->parameter['x_max'];
1115          $data = array();
1116          $data = $this->find_range($this->x_data, $min, $max, $this->parameter['x_resolution']);
1117          $min = $data['min'];
1118          $max = $data['max'];
1119          $this->calculated['x_axis']['min'] = $min;
1120          $this->calculated['x_axis']['max'] = $max;
1121  
1122          $step = ($max - $min) / ($this->calculated['x_axis']['num_ticks'] - 1);
1123          $start = $min;
1124  
1125          for ($i = 0; $i < $this->calculated['x_axis']['num_ticks']; $i++) { // calculate x axis text sizes
1126            $value = number_format($start, $this->parameter['xDecimal'], $this->parameter['decimal_point'], $this->parameter['thousand_sep']);
1127            $this->calculated['x_axis']['data'][$i]  = $start;
1128            $this->calculated['x_axis']['text'][$i]  = $value; // text is formatted raw data
1129  
1130            $size = $this->get_boundaryBox(
1131              array('points' => $axis_size,
1132                    'font'   => $axis_font,
1133                    'angle'  => $axis_angle,
1134                    'colour' => $axis_colour,
1135                    'text'   => $value));
1136            $this->calculated['x_axis']['boundary_box'][$i] = $size;
1137  
1138            if ($size['height'] > $this->calculated['x_axis']['boundary_box_max']['height'])
1139              $this->calculated['x_axis']['boundary_box_max'] = $size;
1140  
1141            $start += $step;
1142          }
1143        }
1144        if ($this->parameter['x_axis_text'])
1145          $this->calculated['boundary_box']['bottom'] -= $this->calculated['x_axis']['boundary_box_max']['height']
1146                                                        + $this->parameter['x_inner_padding'];
1147      }
1148  
1149      // find max and min values for a data array given the resolution.
1150      function find_range($data, $min, $max, $resolution) {
1151        if (sizeof($data) == 0 ) return array('min' => 0, 'max' => 0);
1152        foreach ($data as $key => $value) {
1153          if ($value=='none') continue;
1154          if ($value > $max) $max = $value;
1155          if ($value < $min) $min = $value;
1156        }
1157  
1158        if ($max == 0) {
1159          $factor = 1;
1160        } else {
1161          if ($max < 0) $factor = - pow(10, (floor(log10(abs($max))) + $resolution) );
1162          else $factor = pow(10, (floor(log10(abs($max))) - $resolution) );
1163        }
1164        if ($factor > 0.1) { // To avoid some wierd rounding errors (Moodle)
1165          $factor = round($factor * 1000.0) / 1000.0; // To avoid some wierd rounding errors (Moodle)
1166        } // To avoid some wierd rounding errors (Moodle)
1167  
1168        $max = $factor * @ceil($max / $factor);
1169        $min = $factor * @floor($min / $factor);
1170  
1171        //print "max=$max, min=$min<br />";
1172  
1173        return array('min' => $min, 'max' => $max);
1174      }
1175  
1176      public function __construct() {
1177        if (func_num_args() == 2) {
1178          $this->parameter['width']  = func_get_arg(0);
1179          $this->parameter['height'] = func_get_arg(1);
1180        }
1181        //$this->boundaryBox  = array(
1182        $this->calculated['boundary_box'] = array(
1183          'left'      =>  0,
1184          'top'       =>  0,
1185          'right'     =>  $this->parameter['width'] - 1,
1186          'bottom'    =>  $this->parameter['height'] - 1);
1187  
1188        $this->init_colours();
1189  
1190        //ImageColorTransparent($this->image, $this->colour['white']); // colour for transparency
1191      }
1192  
1193      /**
1194       * Old syntax of class constructor. Deprecated in PHP7.
1195       *
1196       * @deprecated since Moodle 3.1
1197       */
1198      public function graph() {
1199          debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
1200          self::__construct();
1201      }
1202  
1203      /**
1204       * Prepare label's text for GD output.
1205       *
1206       * @param string    $label string to be prepared.
1207       * @return string   Reversed input string, if we are in RTL mode and has no numbers.
1208       *                  Otherwise, returns the string as is.
1209       */
1210      private function prepare_label_text($label) {
1211          if (right_to_left() and !preg_match('/[0-9]/i', $label)) {
1212              return core_text::strrev($label);
1213          } else {
1214              return $label;
1215          }
1216      }
1217  
1218      function print_TTF($message) {
1219        $points    = $message['points'];
1220        $angle     = $message['angle'];
1221        // We have to manually reverse the label, since php GD cannot handle RTL characters properly in UTF8 strings.
1222        $text      = $this->prepare_label_text($message['text']);
1223        $colour    = $this->colour[$message['colour']];
1224        $font      = $this->parameter['path_to_fonts'].$message['font'];
1225  
1226        $x         = $message['boundary_box']['x'];
1227        $y         = $message['boundary_box']['y'];
1228        $offsetX   = $message['boundary_box']['offsetX'];
1229        $offsetY   = $message['boundary_box']['offsetY'];
1230        $height    = $message['boundary_box']['height'];
1231        $width     = $message['boundary_box']['width'];
1232        $reference = $message['boundary_box']['reference'];
1233  
1234        switch ($reference) {
1235          case 'top-left':
1236          case 'left-top':
1237            $y += $height - $offsetY;
1238            //$y += $offsetY;
1239            $x += $offsetX;
1240            break;
1241          case 'left-center':
1242            $y += ($height / 2) - $offsetY;
1243            $x += $offsetX;
1244            break;
1245          case 'left-bottom':
1246            $y -= $offsetY;
1247            $x += $offsetX;
1248           break;
1249          case 'top-center':
1250            $y += $height - $offsetY;
1251            $x -= ($width / 2) - $offsetX;
1252           break;
1253          case 'top-right':
1254          case 'right-top':
1255            $y += $height - $offsetY;
1256            $x -= $width  - $offsetX;
1257            break;
1258          case 'right-center':
1259            $y += ($height / 2) - $offsetY;
1260            $x -= $width  - $offsetX;
1261            break;
1262          case 'right-bottom':
1263            $y -= $offsetY;
1264            $x -= $width  - $offsetX;
1265            break;
1266          case 'bottom-center':
1267            $y -= $offsetY;
1268            $x -= ($width / 2) - $offsetX;
1269           break;
1270          default:
1271            $y = 0;
1272            $x = 0;
1273            break;
1274        }
1275        // start of Moodle addition
1276        $text = core_text::utf8_to_entities($text, true, true); //does not work with hex entities!
1277        // end of Moodle addition
1278        ImageTTFText($this->image, $points, $angle, $x, $y, $colour, $font, $text);
1279      }
1280  
1281      // move boundaryBox to coordinates specified
1282      function update_boundaryBox(&$boundaryBox, $coords) {
1283        $width      = $boundaryBox['width'];
1284        $height     = $boundaryBox['height'];
1285        $x          = $coords['x'];
1286        $y          = $coords['y'];
1287        $reference  = $coords['reference'];
1288        switch ($reference) {
1289          case 'top-left':
1290          case 'left-top':
1291            $top    = $y;
1292            $bottom = $y + $height;
1293            $left   = $x;
1294            $right  = $x + $width;
1295            break;
1296          case 'left-center':
1297            $top    = $y - ($height / 2);
1298            $bottom = $y + ($height / 2);
1299            $left   = $x;
1300            $right  = $x + $width;
1301            break;
1302          case 'left-bottom':
1303            $top    = $y - $height;
1304            $bottom = $y;
1305            $left   = $x;
1306            $right  = $x + $width;
1307            break;
1308          case 'top-center':
1309            $top    = $y;
1310            $bottom = $y + $height;
1311            $left   = $x - ($width / 2);
1312            $right  = $x + ($width / 2);
1313            break;
1314          case 'right-top':
1315          case 'top-right':
1316            $top    = $y;
1317            $bottom = $y + $height;
1318            $left   = $x - $width;
1319            $right  = $x;
1320            break;
1321          case 'right-center':
1322            $top    = $y - ($height / 2);
1323            $bottom = $y + ($height / 2);
1324            $left   = $x - $width;
1325            $right  = $x;
1326            break;
1327          case 'bottom=right':
1328          case 'right-bottom':
1329            $top    = $y - $height;
1330            $bottom = $y;
1331            $left   = $x - $width;
1332            $right  = $x;
1333            break;
1334          default:
1335            $top    = 0;
1336            $bottom = $height;
1337            $left   = 0;
1338            $right  = $width;
1339            break;
1340        }
1341  
1342        $boundaryBox = array_merge($boundaryBox, array('top'       => $top,
1343                                                       'bottom'    => $bottom,
1344                                                       'left'      => $left,
1345                                                       'right'     => $right,
1346                                                       'x'         => $x,
1347                                                       'y'         => $y,
1348                                                       'reference' => $reference));
1349      }
1350  
1351      function get_null_size() {
1352        return array('width'      => 0,
1353                     'height'     => 0,
1354                     'offsetX'    => 0,
1355                     'offsetY'    => 0,
1356                     //'fontHeight' => 0
1357                     );
1358      }
1359  
1360      function get_boundaryBox($message) {
1361        $points  = $message['points'];
1362        $angle   = $message['angle'];
1363        $font    = $this->parameter['path_to_fonts'].$message['font'];
1364        $text    = $message['text'];
1365  
1366        //print ('get_boundaryBox');
1367        //expandPre($message);
1368  
1369        // get font size
1370        $bounds = ImageTTFBBox($points, $angle, $font, "W");
1371        if ($angle < 0) {
1372          $fontHeight = abs($bounds[7]-$bounds[1]);
1373        } else if ($angle > 0) {
1374          $fontHeight = abs($bounds[1]-$bounds[7]);
1375        } else {
1376          $fontHeight = abs($bounds[7]-$bounds[1]);
1377        }
1378  
1379        // get boundary box and offsets for printing at an angle
1380        // start of Moodle addition
1381        $text = core_text::utf8_to_entities($text, true, true); //gd does not work with hex entities!
1382        // end of Moodle addition
1383        $bounds = ImageTTFBBox($points, $angle, $font, $text);
1384  
1385        if ($angle < 0) {
1386          $width = abs($bounds[4]-$bounds[0]);
1387          $height = abs($bounds[3]-$bounds[7]);
1388          $offsetY = abs($bounds[3]-$bounds[1]);
1389          $offsetX = 0;
1390  
1391        } else if ($angle > 0) {
1392          $width = abs($bounds[2]-$bounds[6]);
1393          $height = abs($bounds[1]-$bounds[5]);
1394          $offsetY = 0;
1395          $offsetX = abs($bounds[0]-$bounds[6]);
1396  
1397        } else {
1398          $width = abs($bounds[4]-$bounds[6]);
1399          $height = abs($bounds[7]-$bounds[1]);
1400          $offsetY = $bounds[1];
1401          $offsetX = 0;
1402        }
1403  
1404        //return values
1405        return array('width'      => $width,
1406                     'height'     => $height,
1407                     'offsetX'    => $offsetX,
1408                     'offsetY'    => $offsetY,
1409                     //'fontHeight' => $fontHeight
1410                     );
1411      }
1412  
1413      function draw_rectangle($border, $colour, $type) {
1414        $colour = $this->colour[$colour];
1415        switch ($type) {
1416          case 'fill':    // fill the rectangle
1417            ImageFilledRectangle($this->image, $border['left'], $border['top'], $border['right'], $border['bottom'], $colour);
1418            break;
1419          case 'box':     // all sides
1420            ImageRectangle($this->image, $border['left'], $border['top'], $border['right'], $border['bottom'], $colour);
1421            break;
1422          case 'axis':    // bottom x axis and left y axis
1423            ImageLine($this->image, $border['left'], $border['top'], $border['left'], $border['bottom'], $colour);
1424            ImageLine($this->image, $border['left'], $border['bottom'], $border['right'], $border['bottom'], $colour);
1425            break;
1426          case 'y':       // left y axis only
1427          case 'y-left':
1428            ImageLine($this->image, $border['left'], $border['top'], $border['left'], $border['bottom'], $colour);
1429            break;
1430          case 'y-right': // right y axis only
1431            ImageLine($this->image, $border['right'], $border['top'], $border['right'], $border['bottom'], $colour);
1432            break;
1433          case 'x':       // bottom x axis only
1434            ImageLine($this->image, $border['left'], $border['bottom'], $border['right'], $border['bottom'], $colour);
1435            break;
1436          case 'u':       // u shaped. bottom x axis and both left and right y axis.
1437            ImageLine($this->image, $border['left'], $border['top'], $border['left'], $border['bottom'], $colour);
1438            ImageLine($this->image, $border['right'], $border['top'], $border['right'], $border['bottom'], $colour);
1439            ImageLine($this->image, $border['left'], $border['bottom'], $border['right'], $border['bottom'], $colour);
1440            break;
1441  
1442        }
1443      }
1444  
1445      function init_colours() {
1446        $this->image              = ImageCreate($this->parameter['width'], $this->parameter['height']);
1447        // standard colours
1448        $this->colour['white']    = ImageColorAllocate ($this->image, 0xFF, 0xFF, 0xFF); // first colour is background colour.
1449        $this->colour['black']    = ImageColorAllocate ($this->image, 0x00, 0x00, 0x00);
1450        $this->colour['maroon']   = ImageColorAllocate ($this->image, 0x80, 0x00, 0x00);
1451        $this->colour['green']    = ImageColorAllocate ($this->image, 0x00, 0x80, 0x00);
1452        $this->colour['ltgreen']  = ImageColorAllocate ($this->image, 0x52, 0xF1, 0x7F);
1453        $this->colour['ltltgreen']= ImageColorAllocate ($this->image, 0x99, 0xFF, 0x99);
1454        $this->colour['olive']    = ImageColorAllocate ($this->image, 0x80, 0x80, 0x00);
1455        $this->colour['navy']     = ImageColorAllocate ($this->image, 0x00, 0x00, 0x80);
1456        $this->colour['purple']   = ImageColorAllocate ($this->image, 0x80, 0x00, 0x80);
1457        $this->colour['gray']     = ImageColorAllocate ($this->image, 0x80, 0x80, 0x80);
1458        $this->colour['red']      = ImageColorAllocate ($this->image, 0xFF, 0x00, 0x00);
1459        $this->colour['ltred']    = ImageColorAllocate ($this->image, 0xFF, 0x99, 0x99);
1460        $this->colour['ltltred']  = ImageColorAllocate ($this->image, 0xFF, 0xCC, 0xCC);
1461        $this->colour['orange']   = ImageColorAllocate ($this->image, 0xFF, 0x66, 0x00);
1462        $this->colour['ltorange']   = ImageColorAllocate ($this->image, 0xFF, 0x99, 0x66);
1463        $this->colour['ltltorange'] = ImageColorAllocate ($this->image, 0xFF, 0xcc, 0x99);
1464        $this->colour['lime']     = ImageColorAllocate ($this->image, 0x00, 0xFF, 0x00);
1465        $this->colour['yellow']   = ImageColorAllocate ($this->image, 0xFF, 0xFF, 0x00);
1466        $this->colour['blue']     = ImageColorAllocate ($this->image, 0x00, 0x00, 0xFF);
1467        $this->colour['ltblue']   = ImageColorAllocate ($this->image, 0x00, 0xCC, 0xFF);
1468        $this->colour['ltltblue'] = ImageColorAllocate ($this->image, 0x99, 0xFF, 0xFF);
1469        $this->colour['fuchsia']  = ImageColorAllocate ($this->image, 0xFF, 0x00, 0xFF);
1470        $this->colour['aqua']     = ImageColorAllocate ($this->image, 0x00, 0xFF, 0xFF);
1471        //$this->colour['white']    = ImageColorAllocate ($this->image, 0xFF, 0xFF, 0xFF);
1472        // shades of gray
1473        $this->colour['grayF0']   = ImageColorAllocate ($this->image, 0xF0, 0xF0, 0xF0);
1474        $this->colour['grayEE']   = ImageColorAllocate ($this->image, 0xEE, 0xEE, 0xEE);
1475        $this->colour['grayDD']   = ImageColorAllocate ($this->image, 0xDD, 0xDD, 0xDD);
1476        $this->colour['grayCC']   = ImageColorAllocate ($this->image, 0xCC, 0xCC, 0xCC);
1477        $this->colour['gray33']   = ImageColorAllocate ($this->image, 0x33, 0x33, 0x33);
1478        $this->colour['gray66']   = ImageColorAllocate ($this->image, 0x66, 0x66, 0x66);
1479        $this->colour['gray99']   = ImageColorAllocate ($this->image, 0x99, 0x99, 0x99);
1480  
1481        $this->colour['none']   = 'none';
1482        return true;
1483      }
1484  
1485      function output() {
1486        if ($this->debug) { // for debugging purposes.
1487          //expandPre($this->graph);
1488          //expandPre($this->y_data);
1489          //expandPre($this->x_data);
1490          //expandPre($this->parameter);
1491        } else {
1492  
1493          $expiresSeconds = $this->parameter['seconds_to_live'];
1494          $expiresHours = $this->parameter['hours_to_live'];
1495  
1496          if ($expiresHours || $expiresSeconds) {
1497            $now = mktime (date("H"),date("i"),date("s"),date("m"),date("d"),date("Y"));
1498            $expires = mktime (date("H")+$expiresHours,date("i"),date("s")+$expiresSeconds,date("m"),date("d"),date("Y"));
1499            $expiresGMT = gmdate('D, d M Y H:i:s', $expires).' GMT';
1500            $lastModifiedGMT  = gmdate('D, d M Y H:i:s', $now).' GMT';
1501  
1502            Header('Last-modified: '.$lastModifiedGMT);
1503            Header('Expires: '.$expiresGMT);
1504          }
1505  
1506          if ($this->parameter['file_name'] == 'none') {
1507            switch ($this->parameter['output_format']) {
1508              case 'GIF':
1509                Header("Content-type: image/gif");  // GIF??. switch to PNG guys!!
1510                ImageGIF($this->image);
1511                break;
1512              case 'JPEG':
1513                Header("Content-type: image/jpeg"); // JPEG for line art??. included for completeness.
1514                ImageJPEG($this->image);
1515                break;
1516             default:
1517                Header("Content-type: image/png");  // preferred output format
1518                ImagePNG($this->image);
1519                break;
1520            }
1521          } else {
1522             switch ($this->parameter['output_format']) {
1523              case 'GIF':
1524                ImageGIF($this->image, $this->parameter['file_name'].'.gif');
1525                break;
1526              case 'JPEG':
1527                ImageJPEG($this->image, $this->parameter['file_name'].'.jpg');
1528                break;
1529             default:
1530                ImagePNG($this->image, $this->parameter['file_name'].'.png');
1531                break;
1532            }
1533          }
1534  
1535          ImageDestroy($this->image);
1536        }
1537      } // function output
1538  
1539      function init_variable(&$variable, $value, $default) {
1540        if (!empty($value)) $variable = $value;
1541        else if (isset($default)) $variable = $default;
1542        else unset($variable);
1543      }
1544  
1545      // plot a point. options include square, circle, diamond, triangle, and dot. offset is used for drawing shadows.
1546      // for diamonds and triangles the size should be an even number to get nice look. if odd the points are crooked.
1547      function plot($x, $y, $type, $size, $colour, $offset) {
1548        //print("drawing point of type: $type, at offset: $offset");
1549        $u = $x + $offset;
1550        $v = $this->calculated['inner_border']['bottom'] - $y + $offset;
1551        $half = $size / 2;
1552  
1553        switch ($type) {
1554          case 'square':
1555            ImageFilledRectangle($this->image, $u-$half, $v-$half, $u+$half, $v+$half, $this->colour[$colour]);
1556            break;
1557          case 'square-open':
1558            ImageRectangle($this->image, $u-$half, $v-$half, $u+$half, $v+$half, $this->colour[$colour]);
1559            break;
1560          case 'circle':
1561            ImageArc($this->image, $u, $v, $size, $size, 0, 360, $this->colour[$colour]);
1562            ImageFillToBorder($this->image, $u, $v, $this->colour[$colour], $this->colour[$colour]);
1563            break;
1564          case 'circle-open':
1565            ImageArc($this->image, $u, $v, $size, $size, 0, 360, $this->colour[$colour]);
1566            break;
1567          case 'diamond':
1568            ImageFilledPolygon($this->image, array($u, $v-$half, $u+$half, $v, $u, $v+$half, $u-$half, $v), 4, $this->colour[$colour]);
1569            break;
1570          case 'diamond-open':
1571            ImagePolygon($this->image, array($u, $v-$half, $u+$half, $v, $u, $v+$half, $u-$half, $v), 4, $this->colour[$colour]);
1572            break;
1573          case 'triangle':
1574            ImageFilledPolygon($this->image, array($u, $v-$half, $u+$half, $v+$half, $u-$half, $v+$half), 3, $this->colour[$colour]);
1575            break;
1576          case 'triangle-open':
1577            ImagePolygon($this->image, array($u, $v-$half, $u+$half, $v+$half, $u-$half, $v+$half), 3, $this->colour[$colour]);
1578            break;
1579          case 'dot':
1580            ImageSetPixel($this->image, $u, $v, $this->colour[$colour]);
1581            break;
1582        }
1583      }
1584  
1585      function bar($x, $y, $type, $size, $colour, $offset, $index, $yoffset) {
1586        $index_offset = $this->calculated['bar_offset_index'][$index];
1587        if ( $yoffset ) {
1588          $bar_offsetx = 0;
1589        } else {
1590          $bar_offsetx = $this->calculated['bar_offset_x'][$index_offset];
1591        }
1592        //$this->dbug("drawing bar at offset = $offset : index = $index: bar_offsetx = $bar_offsetx");
1593  
1594        $span = ($this->calculated['bar_width'] * $size) / 2;
1595        $x_left  = $x + $bar_offsetx - $span;
1596        $x_right = $x + $bar_offsetx + $span;
1597  
1598        if ($this->parameter['zero_axis'] != 'none') {
1599          $zero = $this->calculated['zero_axis'];
1600          if ($this->parameter['shadow_below_axis'] ) $zero  += $offset;
1601          $u_left  = $x_left + $offset;
1602          $u_right = $x_right + $offset - 1;
1603          $v       = $this->calculated['boundary_box']['bottom'] - $y + $offset;
1604  
1605          if ($v > $zero) {
1606            $top = $zero +1;
1607            $bottom = $v;
1608          } else {
1609            $top = $v;
1610            $bottom = $zero - 1;
1611          }
1612  
1613          switch ($type) {
1614            case 'open':
1615              //ImageRectangle($this->image, round($u_left), $top, round($u_right), $bottom, $this->colour[$colour]);
1616              if ($v > $zero)
1617                ImageRectangle($this->image, round($u_left), $bottom, round($u_right), $bottom, $this->colour[$colour]);
1618              else
1619                ImageRectangle($this->image, round($u_left), $top, round($u_right), $top, $this->colour[$colour]);
1620              ImageRectangle($this->image, round($u_left), $top, round($u_left), $bottom, $this->colour[$colour]);
1621              ImageRectangle($this->image, round($u_right), $top, round($u_right), $bottom, $this->colour[$colour]);
1622              break;
1623            case 'fill':
1624              ImageFilledRectangle($this->image, round($u_left), $top, round($u_right), $bottom, $this->colour[$colour]);
1625              break;
1626          }
1627  
1628        } else {
1629  
1630          $bottom = $this->calculated['boundary_box']['bottom'];
1631          if ($this->parameter['shadow_below_axis'] ) $bottom  += $offset;
1632          if ($this->parameter['inner_border'] != 'none') $bottom -= 1; // 1 pixel above bottom if border is to be drawn.
1633          $u_left  = $x_left + $offset;
1634          $u_right = $x_right + $offset - 1;
1635          $v       = $this->calculated['boundary_box']['bottom'] - $y + $offset;
1636  
1637          // Moodle addition, plus the function parameter yoffset
1638          if ($yoffset) {                                           // Moodle
1639              $yoffset = $yoffset - round(($bottom - $v) / 2.0);    // Moodle
1640              $bottom -= $yoffset;                                  // Moodle
1641              $v      -= $yoffset;                                  // Moodle
1642          }                                                         // Moodle
1643  
1644          switch ($type) {
1645            case 'open':
1646              ImageRectangle($this->image, round($u_left), $v, round($u_right), $bottom, $this->colour[$colour]);
1647              break;
1648            case 'fill':
1649              ImageFilledRectangle($this->image, round($u_left), $v, round($u_right), $bottom, $this->colour[$colour]);
1650              break;
1651          }
1652        }
1653      }
1654  
1655      function area($x_start, $y_start, $x_end, $y_end, $type, $colour, $offset) {
1656        //dbug("drawing area type: $type, at offset: $offset");
1657        if ($this->parameter['zero_axis'] != 'none') {
1658          $bottom = $this->calculated['boundary_box']['bottom'];
1659          $zero   = $this->calculated['zero_axis'];
1660          if ($this->parameter['shadow_below_axis'] ) $zero  += $offset;
1661          $u_start = $x_start + $offset;
1662          $u_end   = $x_end + $offset;
1663          $v_start = $bottom - $y_start + $offset;
1664          $v_end   = $bottom - $y_end + $offset;
1665          switch ($type) {
1666            case 'fill':
1667              // draw it this way 'cos the FilledPolygon routine seems a bit buggy.
1668              ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), 4, $this->colour[$colour]);
1669              ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), 4, $this->colour[$colour]);
1670             break;
1671            case 'open':
1672              //ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), 4, $this->colour[$colour]);
1673              ImageLine($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]);
1674              ImageLine($this->image, $u_start, $v_start, $u_start, $zero, $this->colour[$colour]);
1675              ImageLine($this->image, $u_end, $v_end, $u_end, $zero, $this->colour[$colour]);
1676             break;
1677          }
1678        } else {
1679          $bottom = $this->calculated['boundary_box']['bottom'];
1680          $u_start = $x_start + $offset;
1681          $u_end   = $x_end + $offset;
1682          $v_start = $bottom - $y_start + $offset;
1683          $v_end   = $bottom - $y_end + $offset;
1684  
1685          if ($this->parameter['shadow_below_axis'] ) $bottom  += $offset;
1686          if ($this->parameter['inner_border'] != 'none') $bottom -= 1; // 1 pixel above bottom if border is to be drawn.
1687          switch ($type) {
1688            case 'fill':
1689              ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), 4, $this->colour[$colour]);
1690             break;
1691            case 'open':
1692              ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), 4, $this->colour[$colour]);
1693             break;
1694          }
1695        }
1696      }
1697  
1698      function line($x_start, $y_start, $x_end, $y_end, $type, $brush_type, $brush_size, $colour, $offset) {
1699        //dbug("drawing line of type: $type, at offset: $offset");
1700        $u_start = $x_start + $offset;
1701        $v_start = $this->calculated['boundary_box']['bottom'] - $y_start + $offset;
1702        $u_end   = $x_end + $offset;
1703        $v_end   = $this->calculated['boundary_box']['bottom'] - $y_end + $offset;
1704  
1705        switch ($type) {
1706          case 'brush':
1707            $this->draw_brush_line($u_start, $v_start, $u_end, $v_end, $brush_size, $brush_type, $colour);
1708           break;
1709          case 'line' :
1710            ImageLine($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]);
1711            break;
1712          case 'dash':
1713            ImageDashedLine($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]);
1714            break;
1715        }
1716      }
1717  
1718      // function to draw line. would prefer to use gdBrush but this is not supported yet.
1719      function draw_brush_line($x0, $y0, $x1, $y1, $size, $type, $colour) {
1720        //$this->dbug("line: $x0, $y0, $x1, $y1");
1721        $dy = $y1 - $y0;
1722        $dx = $x1 - $x0;
1723        $t = 0;
1724        $watchdog = 1024; // precaution to prevent infinite loops.
1725  
1726        $this->draw_brush($x0, $y0, $size, $type, $colour);
1727        if (abs($dx) > abs($dy)) { // slope < 1
1728          //$this->dbug("slope < 1");
1729          $m = $dy / $dx; // compute slope
1730          $t += $y0;
1731          $dx = ($dx < 0) ? -1 : 1;
1732          $m *= $dx;
1733          while (round($x0) != round($x1)) {
1734            if (!$watchdog--) break;
1735            $x0 += $dx; // step to next x value
1736            $t += $m;   // add slope to y value
1737            $y = round($t);
1738            //$this->dbug("x0=$x0, x1=$x1, y=$y watchdog=$watchdog");
1739            $this->draw_brush($x0, $y, $size, $type, $colour);
1740  
1741          }
1742        } else { // slope >= 1
1743          //$this->dbug("slope >= 1");
1744          $m = $dx / $dy; // compute slope
1745          $t += $x0;
1746          $dy = ($dy < 0) ? -1 : 1;
1747          $m *= $dy;
1748          while (round($y0) != round($y1)) {
1749            if (!$watchdog--) break;
1750            $y0 += $dy; // step to next y value
1751            $t += $m;   // add slope to x value
1752            $x = round($t);
1753            //$this->dbug("x=$x, y0=$y0, y1=$y1 watchdog=$watchdog");
1754            $this->draw_brush($x, $y0, $size, $type, $colour);
1755  
1756          }
1757        }
1758      }
1759  
1760      function draw_brush($x, $y, $size, $type, $colour) {
1761        $x = round($x);
1762        $y = round($y);
1763        $half = round($size / 2);
1764        switch ($type) {
1765          case 'circle':
1766            ImageArc($this->image, $x, $y, $size, $size, 0, 360, $this->colour[$colour]);
1767            ImageFillToBorder($this->image, $x, $y, $this->colour[$colour], $this->colour[$colour]);
1768            break;
1769          case 'square':
1770            ImageFilledRectangle($this->image, $x-$half, $y-$half, $x+$half, $y+$half, $this->colour[$colour]);
1771            break;
1772          case 'vertical':
1773            ImageFilledRectangle($this->image, $x, $y-$half, $x+1, $y+$half, $this->colour[$colour]);
1774            break;
1775          case 'horizontal':
1776            ImageFilledRectangle($this->image, $x-$half, $y, $x+$half, $y+1, $this->colour[$colour]);
1777            break;
1778          case 'slash':
1779            ImageFilledPolygon($this->image, array($x+$half, $y-$half,
1780                                                   $x+$half+1, $y-$half,
1781                                                   $x-$half+1, $y+$half,
1782                                                   $x-$half, $y+$half
1783                                                   ), 4, $this->colour[$colour]);
1784            break;
1785          case 'backslash':
1786            ImageFilledPolygon($this->image, array($x-$half, $y-$half,
1787                                                   $x-$half+1, $y-$half,
1788                                                   $x+$half+1, $y+$half,
1789                                                   $x+$half, $y+$half
1790                                                   ), 4, $this->colour[$colour]);
1791            break;
1792          default:
1793            @eval($type); // user can create own brush script.
1794        }
1795      }
1796  
1797  } // class graph