Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
/lib/ -> graphlib.php (source)

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