Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
/lib/ -> graphlib.php (source)

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]

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