<?php
/**
 * Graph Class. PHP Class to draw line, point, bar, and area graphs, including numeric x-axis and double y-axis.
 * Version: 1.6.3
 * Copyright (C) 2000  Herman Veluwenkamp
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Copy of GNU Lesser General Public License at: http://www.gnu.org/copyleft/lesser.txt
 * Contact author at: hermanV@mindless.com
 *
 * @package    core
 * @subpackage lib
 */

declare(strict_types=1);

defined('MOODLE_INTERNAL') || die();

/* This file contains modifications by Martin Dougiamas
 * as part of Moodle (http://moodle.com).  Modified lines
 * are marked with "Moodle".
 */

/**
 * @package moodlecore
 */
class graph {
  var $image;
  var $debug             =   FALSE;        // be careful!!
  var $calculated        =   array();      // array of computed values for chart
  var $parameter         =   array(        // input parameters
    'width'              =>  320,          // default width of image
    'height'             =>  240,          // default height of image
    'file_name'          => 'none',        // name of file for file to be saved as.
                                           //  NOTE: no suffix required. this is determined from output_format below.
    'output_format'      => 'PNG',         // image output format. 'GIF', 'PNG', 'JPEG'. default 'PNG'.

    'seconds_to_live'    =>  0,            // expiry time in seconds (for HTTP header)
    'hours_to_live'      =>  0,            // expiry time in hours (for HTTP header)
    'path_to_fonts'      => 'fonts/',      // path to fonts folder. don't forget *trailing* slash!!
                                           //   for WINDOZE this may need to be the full path, not relative.

    'title'              => 'Graph Title', // text for graph title
    'title_font'         => 'default.ttf',   // title text font. don't forget to set 'path_to_fonts' above.
    'title_size'         =>  16,           // title text point size
    'title_colour'       => 'black',       // colour for title text

    'x_label'            => '',            // if this is set then this text is printed on bottom axis of graph.
    'y_label_left'       => '',            // if this is set then this text is printed on left axis of graph.
    'y_label_right'      => '',            // if this is set then this text is printed on right axis of graph.

    'label_size'         =>  8,           // label text point size
    'label_font'         => 'default.ttf', // label text font. don't forget to set 'path_to_fonts' above.
    'label_colour'       => 'gray33',      // label text colour
    'y_label_angle'      =>  90,           // rotation of y axis label

    'x_label_angle'      =>  90,            // rotation of y axis label

    'outer_padding'      =>  5,            // padding around outer text. i.e. title, y label, and x label.
    'inner_padding'      =>  0,            // padding beteen axis text and graph.
    'x_inner_padding'      =>  5,            // padding beteen axis text and graph.
    'y_inner_padding'      =>  6,            // padding beteen axis text and graph.
    'outer_border'       => 'none',        // colour of border aound image, or 'none'.
    'inner_border'       => 'black',       // colour of border around actual graph, or 'none'.
    'inner_border_type'  => 'box',         // 'box' for all four sides, 'axis' for x/y axis only,
                                           // 'y' or 'y-left' for y axis only, 'y-right' for right y axis only,
                                           // 'x' for x axis only, 'u' for both left and right y axis and x axis.
    'outer_background'   => 'none',        // background colour of entire image.
    'inner_background'   => 'none',        // background colour of plot area.

    'y_min_left'         =>  0,            // this will be reset to minimum value if there is a value lower than this.
    'y_max_left'         =>  0,            // this will be reset to maximum value if there is a value higher than this.
    'y_min_right'        =>  0,            // this will be reset to minimum value if there is a value lower than this.
    'y_max_right'        =>  0,            // this will be reset to maximum value if there is a value higher than this.
    'x_min'              =>  0,            // only used if x axis is numeric.
    'x_max'              =>  0,            // only used if x axis is numeric.

    'y_resolution_left'  =>  1,            // scaling for rounding of y axis max value.
                                           // if max y value is 8645 then
                                           // if y_resolution is 0, then y_max becomes 9000.
                                           // if y_resolution is 1, then y_max becomes 8700.
                                           // if y_resolution is 2, then y_max becomes 8650.
                                           // if y_resolution is 3, then y_max becomes 8645.
                                           // get it?
    'y_decimal_left'     =>  0,            // number of decimal places for y_axis text.
    'y_resolution_right' =>  2,            // ... same for right hand side
    'y_decimal_right'    =>  0,            // ... same for right hand side
    'x_resolution'       =>  2,            // only used if x axis is numeric.
    'x_decimal'          =>  0,            // only used if x axis is numeric.

    'point_size'         =>  4,            // default point size. use even number for diamond or triangle to get nice look.
    'brush_size'         =>  4,            // default brush size for brush line.
    'brush_type'         => 'circle',      // type of brush to use to draw line. choose from the following
                                           //   'circle', 'square', 'horizontal', 'vertical', 'slash', 'backslash'
    'bar_size'           =>  0.8,          // size of bar to draw. <1 bars won't touch
                                           //   1 is full width - i.e. bars will touch.
                                           //   >1 means bars will overlap.
    'bar_spacing'        =>  10,           // space in pixels between group of bars for each x value.
    'shadow_offset'      =>  3,            // draw shadow at this offset, unless overidden by data parameter.
    'shadow'             => 'grayCC',      // 'none' or colour of shadow.
    'shadow_below_axis'  => true,         // whether to draw shadows of bars and areas below the x/zero axis.


    'x_axis_gridlines'   => 'auto',        // if set to a number then x axis is treated as numeric.
    'y_axis_gridlines'   =>  6,            // number of gridlines on y axis.
    'zero_axis'          => 'none',        // colour to draw zero-axis, or 'none'.


    'axis_font'          => 'default.ttf', // axis text font. don't forget to set 'path_to_fonts' above.
    'axis_size'          =>  8,            // axis text font size in points
    'axis_colour'        => 'gray33',      // colour of axis text.
    'y_axis_angle'       =>  0,            // rotation of axis text.
    'x_axis_angle'       =>  0,            // rotation of axis text.

    'y_axis_text_left'   =>  1,            // whether to print left hand y axis text. if 0 no text, if 1 all ticks have text,
    'x_axis_text'        =>  1,            //   if 4 then print every 4th tick and text, etc...
    'y_axis_text_right'  =>  0,            // behaviour same as above for right hand y axis.

    'x_offset'           =>  0.5,          // x axis tick offset from y axis as fraction of tick spacing.
    'y_ticks_colour'     => 'black',       // colour to draw y ticks, or 'none'
    'x_ticks_colour'     => 'black',       // colour to draw x ticks, or 'none'
    'y_grid'             => 'line',        // grid lines. set to 'line' or 'dash'...
    'x_grid'             => 'line',        //   or if set to 'none' print nothing.
    'grid_colour'        => 'grayEE',      // default grid colour.
    'tick_length'        =>  4,            // length of ticks in pixels. can be negative. i.e. outside data drawing area.

    'legend'             => 'none',        // default. no legend.
                                          // otherwise: 'top-left', 'top-right', 'bottom-left', 'bottom-right',
                                          //   'outside-top', 'outside-bottom', 'outside-left', or 'outside-right'.
    'legend_offset'      =>  10,           // offset in pixels from graph or outside border.
    'legend_padding'     =>  5,            // padding around legend text.
    'legend_font'        => 'default.ttf',   // legend text font. don't forget to set 'path_to_fonts' above.
    'legend_size'        =>  8,            // legend text point size.
    'legend_colour'      => 'black',       // legend text colour.
    'legend_border'      => 'none',        // legend border colour, or 'none'.

    'decimal_point'      => '.',           // symbol for decimal separation  '.' or ',' *european support.
    'thousand_sep'       => ',',           // symbol for thousand separation ',' or ''

  );
  var $y_tick_labels     =   null;         // array of text values for y-axis tick labels
  var $offset_relation   =   null;         // array of offsets for different sets of data

  /** @var array y_order data. */
  public $y_order = [];

  /** @var array y_format data. */
  public $y_format = [];

  /** @var array x_data data. */
  public $x_data = [];

  /** @var array colour. */
  public $colour = [];

  /** @var array y_data data. */
  public $y_data = [];

    // init all text - title, labels, and axis text.
    function init() {

      /// Moodle mods:  overrides the font path and encodings

      global $CFG;

      /// A default.ttf is searched for in this order:
      ///      dataroot/lang/xx_local/fonts
      ///      dataroot/lang/xx/fonts
      ///      dirroot/lang/xx/fonts
      ///      dataroot/lang
      ///      lib/

      $currlang = current_language();
      if (file_exists("$CFG->dataroot/lang/".$currlang."_local/fonts/default.ttf")) {
          $fontpath = "$CFG->dataroot/lang/".$currlang."_local/fonts/";
      } else if (file_exists("$CFG->dataroot/lang/$currlang/fonts/default.ttf")) {
          $fontpath = "$CFG->dataroot/lang/$currlang/fonts/";
      } else if (file_exists("$CFG->dirroot/lang/$currlang/fonts/default.ttf")) {
          $fontpath = "$CFG->dirroot/lang/$currlang/fonts/";
      } else if (file_exists("$CFG->dataroot/lang/default.ttf")) {
          $fontpath = "$CFG->dataroot/lang/";
      } else {
          $fontpath = "$CFG->libdir/";
      }

      $this->parameter['path_to_fonts'] = $fontpath;

      /// End Moodle mods



      $this->calculated['outer_border'] = $this->calculated['boundary_box'];

      // outer padding
      $this->calculated['boundary_box']['left']   += $this->parameter['outer_padding'];
      $this->calculated['boundary_box']['top']    += $this->parameter['outer_padding'];
      $this->calculated['boundary_box']['right']  -= $this->parameter['outer_padding'];
      $this->calculated['boundary_box']['bottom'] -= $this->parameter['outer_padding'];

      $this->init_x_axis();
      $this->init_y_axis();
      $this->init_legend();
      $this->init_labels();

      //  take into account tick lengths
      $this->calculated['bottom_inner_padding'] = $this->parameter['x_inner_padding'];
      if (($this->parameter['x_ticks_colour'] != 'none') && ($this->parameter['tick_length'] < 0))
        $this->calculated['bottom_inner_padding'] -= $this->parameter['tick_length'];
      $this->calculated['boundary_box']['bottom'] -= $this->calculated['bottom_inner_padding'];

      $this->calculated['left_inner_padding'] = $this->parameter['y_inner_padding'];
      if ($this->parameter['y_axis_text_left']) {
        if (($this->parameter['y_ticks_colour'] != 'none') && ($this->parameter['tick_length'] < 0))
          $this->calculated['left_inner_padding'] -= $this->parameter['tick_length'];
      }
      $this->calculated['boundary_box']['left'] += $this->calculated['left_inner_padding'];

      $this->calculated['right_inner_padding'] = $this->parameter['y_inner_padding'];
      if ($this->parameter['y_axis_text_right']) {
        if (($this->parameter['y_ticks_colour'] != 'none') && ($this->parameter['tick_length'] < 0))
          $this->calculated['right_inner_padding'] -= $this->parameter['tick_length'];
      }
      $this->calculated['boundary_box']['right'] -= $this->calculated['right_inner_padding'];

      // boundaryBox now has coords for plotting area.
      $this->calculated['inner_border'] = $this->calculated['boundary_box'];

      $this->init_data();
      $this->init_x_ticks();
      $this->init_y_ticks();
    }

    function draw_text() {
      $colour = $this->parameter['outer_background'];
      if ($colour != 'none') $this->draw_rectangle($this->calculated['outer_border'], $colour, 'fill'); // graph background

      // draw border around image
      $colour = $this->parameter['outer_border'];
      if ($colour != 'none') $this->draw_rectangle($this->calculated['outer_border'], $colour, 'box'); // graph border

      $this->draw_title();
      $this->draw_x_label();
      $this->draw_y_label_left();
      $this->draw_y_label_right();
      $this->draw_x_axis();
      $this->draw_y_axis();
      if      ($this->calculated['y_axis_left']['has_data'])  $this->draw_zero_axis_left();  // either draw zero axis on left
      else if ($this->calculated['y_axis_right']['has_data']) $this->draw_zero_axis_right(); // ... or right.
      $this->draw_legend();

      // draw border around plot area
      $colour = $this->parameter['inner_background'];
      if ($colour != 'none') $this->draw_rectangle($this->calculated['inner_border'], $colour, 'fill'); // graph background

      // draw border around image
      $colour = $this->parameter['inner_border'];
      if ($colour != 'none') $this->draw_rectangle($this->calculated['inner_border'], $colour, $this->parameter['inner_border_type']); // graph border
    }

    function draw_stack() {
      $this->init();
      $this->draw_text();

      $yOrder = $this->y_order; // save y_order data.
      // iterate over each data set. order is very important if you want to see data correctly. remember shadows!!
      foreach ($yOrder as $set) {
        $this->y_order = array($set);
        $this->init_data();
        $this->draw_data();
      }
      $this->y_order = $yOrder; // revert y_order data.

      $this->output();
    }

    function draw() {
      $this->init();
      $this->draw_text();
      $this->draw_data();
      $this->output();
    }

    // draw a data set
    function draw_set($order, $set, $offset) {
      if ($offset) @$this->init_variable($colour, $this->y_format[$set]['shadow'], $this->parameter['shadow']);
      else $colour  = $this->y_format[$set]['colour'];
      @$this->init_variable($point,      $this->y_format[$set]['point'],      'none');
      @$this->init_variable($pointSize,  $this->y_format[$set]['point_size'],  $this->parameter['point_size']);
      @$this->init_variable($line,       $this->y_format[$set]['line'],       'none');
      @$this->init_variable($brushType,  $this->y_format[$set]['brush_type'],  $this->parameter['brush_type']);
      @$this->init_variable($brushSize,  $this->y_format[$set]['brush_size'],  $this->parameter['brush_size']);
      @$this->init_variable($bar,        $this->y_format[$set]['bar'],        'none');
      @$this->init_variable($barSize,    $this->y_format[$set]['bar_size'],    $this->parameter['bar_size']);
      @$this->init_variable($area,       $this->y_format[$set]['area'],       'none');

      $lastX = 0;
      $lastY = 'none';
      $fromX = 0;
      $fromY = 'none';

      //print "set $set<br />";
      //expand_pre($this->calculated['y_plot']);

      foreach ($this->x_data as $index => $x) {
        //print "index $index<br />";
        $thisY = $this->calculated['y_plot'][$set][$index];
        $thisX = $this->calculated['x_plot'][$index];

        //print "$thisX, $thisY <br />";

        if (($bar!='none') && (string)$thisY != 'none') {
            if (isset($this->offset_relation[$set]) && $relatedset = $this->offset_relation[$set]) {
                $yoffset = $this->calculated['y_plot'][$relatedset][$index];                // Moodle
            } else {                                                                        // Moodle
                $yoffset = 0;                                                               // Moodle
            }                                                                               // Moodle
            //$this->bar($thisX, $thisY, $bar, $barSize, $colour, $offset, $set);           // Moodle
            $this->bar($thisX, $thisY, $bar, $barSize, $colour, $offset, $set, $yoffset);   // Moodle
        }

        if (($area!='none') && (((string)$lastY != 'none') && ((string)$thisY != 'none')))
          $this->area($lastX, $lastY, $thisX, $thisY, $area, $colour, $offset);

        if (($point!='none') && (string)$thisY != 'none') $this->plot($thisX, $thisY, $point, $pointSize, $colour, $offset);

        if (($line!='none') && ((string)$thisY != 'none')) {
          if ((string)$fromY != 'none')
            $this->line($fromX, $fromY, $thisX, $thisY, $line, $brushType, $brushSize, $colour, $offset);

          $fromY = $thisY; // start next line from here
          $fromX = $thisX; // ...
        } else {
          $fromY = 'none';
          $fromX = 'none';
        }

        $lastX = $thisX;
        $lastY = $thisY;
      }
    }

    function draw_data() {
      // cycle thru y data to be plotted
      // first check for drop shadows...
      foreach ($this->y_order as $order => $set) {
        @$this->init_variable($offset, $this->y_format[$set]['shadow_offset'], $this->parameter['shadow_offset']);
        @$this->init_variable($colour, $this->y_format[$set]['shadow'], $this->parameter['shadow']);
        if ($colour != 'none') $this->draw_set($order, $set, $offset);

      }

      // then draw data
      foreach ($this->y_order as $order => $set) {
        $this->draw_set($order, $set, 0);
      }
    }

    function draw_legend() {
      $position      = $this->parameter['legend'];
      if ($position == 'none') return; // abort if no border

      $borderColour  = $this->parameter['legend_border'];
      $offset        = $this->parameter['legend_offset'];
      $padding       = $this->parameter['legend_padding'];
      $height        = $this->calculated['legend']['boundary_box_all']['height'];
      $width         = $this->calculated['legend']['boundary_box_all']['width'];
      $graphTop      = $this->calculated['boundary_box']['top'];
      $graphBottom   = $this->calculated['boundary_box']['bottom'];
      $graphLeft     = $this->calculated['boundary_box']['left'];
      $graphRight    = $this->calculated['boundary_box']['right'];
      $outsideRight  = $this->calculated['outer_border']['right'];
      $outsideBottom = $this->calculated['outer_border']['bottom'];
      switch ($position) {
        case 'top-left':
          $top    = $graphTop  + $offset;
          $bottom = $graphTop  + $height + $offset;
          $left   = $graphLeft + $offset;
          $right  = $graphLeft + $width + $offset;

          break;
        case 'top-right':
          $top    = $graphTop   + $offset;
          $bottom = $graphTop   + $height + $offset;
          $left   = $graphRight - $width - $offset;
          $right  = $graphRight - $offset;

          break;
        case 'bottom-left':
          $top    = $graphBottom - $height - $offset;
          $bottom = $graphBottom - $offset;
          $left   = $graphLeft   + $offset;
          $right  = $graphLeft   + $width + $offset;

          break;
        case 'bottom-right':
          $top    = $graphBottom - $height - $offset;
          $bottom = $graphBottom - $offset;
          $left   = $graphRight  - $width - $offset;
          $right  = $graphRight  - $offset;
          break;

        case 'outside-top' :
          $top    = $graphTop;
          $bottom = $graphTop     + $height;
          $left   = $outsideRight - $width - $offset;
          $right  = $outsideRight - $offset;
          break;

        case 'outside-bottom' :
          $top    = $graphBottom  - $height;
          $bottom = $graphBottom;
          $left   = $outsideRight - $width - $offset;
          $right  = $outsideRight - $offset;
         break;

        case 'outside-left' :
          $top    = $outsideBottom - $height - $offset;
          $bottom = $outsideBottom - $offset;
          $left   = $graphLeft;
          $right  = $graphLeft     + $width;
         break;

        case 'outside-right' :
          $top    = $outsideBottom - $height - $offset;
          $bottom = $outsideBottom - $offset;
          $left   = $graphRight    - $width;
          $right  = $graphRight;
          break;
        default: // default is top left. no particular reason.
          $top    = $this->calculated['boundary_box']['top'];
          $bottom = $this->calculated['boundary_box']['top'] + $this->calculated['legend']['boundary_box_all']['height'];
          $left   = $this->calculated['boundary_box']['left'];
          $right  = $this->calculated['boundary_box']['right'] + $this->calculated['legend']['boundary_box_all']['width'];

    }
      // legend border
      if($borderColour!='none') $this->draw_rectangle(array('top' => $top,
                                                            'left' => $left,
                                                            'bottom' => $bottom,
                                                            'right' => $right), $this->parameter['legend_border'], 'box');

      // legend text
      $legendText = array('points' => $this->parameter['legend_size'],
                          'angle'  => 0,
                          'font'   => $this->parameter['legend_font'],
                          'colour' => $this->parameter['legend_colour']);

      $box = $this->calculated['legend']['boundary_box_max']['height']; // use max height for legend square size.
      $x = $left + $padding;
      $x_text = $x + $box * 2;
      $y = $top + $padding;

      foreach ($this->y_order as $set) {
        $legendText['text'] = $this->calculated['legend']['text'][$set];
        if ($legendText['text'] != 'none') {
          // if text exists then draw box and text
          $boxColour = $this->colour[$this->y_format[$set]['colour']];

          // draw box
          ImageFilledRectangle($this->image, $x, $y, $x + $box, $y + $box, $boxColour);

          // draw text
          $coords = array('x' => $x + $box * 2, 'y' => $y, 'reference' => 'top-left');
          $legendText['boundary_box'] = $this->calculated['legend']['boundary_box'][$set];
          $this->update_boundaryBox($legendText['boundary_box'], $coords);
          $this->print_TTF($legendText);
          $y += $padding + $box;
        }
      }

    }

    function draw_y_label_right() {
      if (!$this->parameter['y_label_right']) return;
      $x = $this->calculated['boundary_box']['right'] + $this->parameter['y_inner_padding'];
      if ($this->parameter['y_axis_text_right']) $x += $this->calculated['y_axis_right']['boundary_box_max']['width']
                                               + $this->calculated['right_inner_padding'];
      $y = ($this->calculated['boundary_box']['bottom'] + $this->calculated['boundary_box']['top']) / 2;

      $label = $this->calculated['y_label_right'];
      $coords = array('x' => $x, 'y' => $y, 'reference' => 'left-center');
      $this->update_boundaryBox($label['boundary_box'], $coords);
      $this->print_TTF($label);
    }


    function draw_y_label_left() {
      if (!$this->parameter['y_label_left']) return;
      $x = $this->calculated['boundary_box']['left'] - $this->parameter['y_inner_padding'];
      if ($this->parameter['y_axis_text_left']) $x -= $this->calculated['y_axis_left']['boundary_box_max']['width']
                                               + $this->calculated['left_inner_padding'];
      $y = ($this->calculated['boundary_box']['bottom'] + $this->calculated['boundary_box']['top']) / 2;

      $label = $this->calculated['y_label_left'];
      $coords = array('x' => $x, 'y' => $y, 'reference' => 'right-center');
      $this->update_boundaryBox($label['boundary_box'], $coords);
      $this->print_TTF($label);
    }

    function draw_title() {
      if (!$this->parameter['title']) return;
      //$y = $this->calculated['outside_border']['top'] + $this->parameter['outer_padding'];
      $y = $this->calculated['boundary_box']['top'] - $this->parameter['outer_padding'];
      $x = ($this->calculated['boundary_box']['right'] + $this->calculated['boundary_box']['left']) / 2;
      $label = $this->calculated['title'];
      $coords = array('x' => $x, 'y' => $y, 'reference' => 'bottom-center');
      $this->update_boundaryBox($label['boundary_box'], $coords);
      $this->print_TTF($label);
    }

    function draw_x_label() {
      if (!$this->parameter['x_label']) return;
      $y = $this->calculated['boundary_box']['bottom'] + $this->parameter['x_inner_padding'];
      if ($this->parameter['x_axis_text']) $y += $this->calculated['x_axis']['boundary_box_max']['height']
                                              + $this->calculated['bottom_inner_padding'];
      $x = ($this->calculated['boundary_box']['right'] + $this->calculated['boundary_box']['left']) / 2;
      $label = $this->calculated['x_label'];
      $coords = array('x' => $x, 'y' => $y, 'reference' => 'top-center');
      $this->update_boundaryBox($label['boundary_box'], $coords);
      $this->print_TTF($label);
    }

    function draw_zero_axis_left() {
      $colour = $this->parameter['zero_axis'];
      if ($colour == 'none') return;
      // draw zero axis on left hand side
      $this->calculated['zero_axis'] = (int) round(
        $this->calculated['boundary_box']['top'] +
        ($this->calculated['y_axis_left']['max'] * $this->calculated['y_axis_left']['factor'])
      );
      ImageLine($this->image, $this->calculated['boundary_box']['left'], $this->calculated['zero_axis'], $this->calculated['boundary_box']['right'], $this->calculated['zero_axis'], $this->colour[$colour]);
    }

    function draw_zero_axis_right() {
      $colour = $this->parameter['zero_axis'];
      if ($colour == 'none') return;
      // draw zero axis on right hand side
      $this->calculated['zero_axis'] = (int) round(
        $this->calculated['boundary_box']['top'] +
        ($this->calculated['y_axis_right']['max'] * $this->calculated['y_axis_right']['factor'])
      );
      ImageLine($this->image, $this->calculated['boundary_box']['left'], $this->calculated['zero_axis'], $this->calculated['boundary_box']['right'], $this->calculated['zero_axis'], $this->colour[$colour]);
    }

    function draw_x_axis() {
      $gridColour  = $this->colour[$this->parameter['grid_colour']];
      $tickColour  = $this->colour[$this->parameter['x_ticks_colour']];
      $axis_colour  = $this->parameter['axis_colour'];
      $xGrid       = $this->parameter['x_grid'];
      $gridTop     = (int) round($this->calculated['boundary_box']['top']);
      $gridBottom  = (int) round($this->calculated['boundary_box']['bottom']);

      if ($this->parameter['tick_length'] >= 0) {
        $tickTop     = $this->calculated['boundary_box']['bottom'] - $this->parameter['tick_length'];
        $tickBottom  = $this->calculated['boundary_box']['bottom'];
        $textBottom  = $tickBottom + $this->calculated['bottom_inner_padding'];
      } else {
        $tickTop     = $this->calculated['boundary_box']['bottom'];
        $tickBottom  = $this->calculated['boundary_box']['bottom'] - $this->parameter['tick_length'];
        $textBottom  = $tickBottom + $this->calculated['bottom_inner_padding'];
      }

      $axis_font    = $this->parameter['axis_font'];
      $axis_size    = $this->parameter['axis_size'];
      $axis_angle   = $this->parameter['x_axis_angle'];

      if ($axis_angle == 0)  $reference = 'top-center';
      if ($axis_angle > 0)   $reference = 'top-right';
      if ($axis_angle < 0)   $reference = 'top-left';
      if ($axis_angle == 90) $reference = 'top-center';

      //generic tag information. applies to all axis text.
      $axisTag = array('points' => $axis_size, 'angle' => $axis_angle, 'font' => $axis_font, 'colour' => $axis_colour);

      foreach ($this->calculated['x_axis']['tick_x'] as $set => $tickX) {
        $tickX = (int) round($tickX);
        // draw x grid if colour specified
        if ($xGrid != 'none') {
          switch ($xGrid) {
            case 'line':
              ImageLine($this->image, $tickX, $gridTop, $tickX, $gridBottom, $gridColour);
              break;
             case 'dash':
              $this->image_dashed_line($this->image, $tickX, $gridTop, $tickX, $gridBottom, $gridColour); // Moodle
              break;
          }
        }

        if ($this->parameter['x_axis_text'] && !($set % $this->parameter['x_axis_text'])) { // test if tick should be displayed
          // draw tick
          if ($tickColour != 'none')
            ImageLine($this->image, $tickX, $tickTop, $tickX, $tickBottom, $tickColour);

          // draw axis text
          $coords = array('x' => $tickX, 'y' => $textBottom, 'reference' => $reference);
          $axisTag['text'] = $this->calculated['x_axis']['text'][$set];
          $axisTag['boundary_box'] = $this->calculated['x_axis']['boundary_box'][$set];
          $this->update_boundaryBox($axisTag['boundary_box'], $coords);
          $this->print_TTF($axisTag);
        }
      }
    }

    function draw_y_axis() {
      $gridColour  = $this->colour[$this->parameter['grid_colour']];
      $tickColour  = $this->colour[$this->parameter['y_ticks_colour']];
      $axis_colour  = $this->parameter['axis_colour'];
      $yGrid       = $this->parameter['y_grid'];
      $gridLeft    = (int) round($this->calculated['boundary_box']['left']);
      $gridRight   = (int) round($this->calculated['boundary_box']['right']);

      // axis font information
      $axis_font    = $this->parameter['axis_font'];
      $axis_size    = $this->parameter['axis_size'];
      $axis_angle   = $this->parameter['y_axis_angle'];
      $axisTag = array('points' => $axis_size, 'angle' => $axis_angle, 'font' => $axis_font, 'colour' => $axis_colour);


      if ($this->calculated['y_axis_left']['has_data']) {
        // LEFT HAND SIDE
        // left and right coords for ticks
        if ($this->parameter['tick_length'] >= 0) {
          $tickLeft     = $this->calculated['boundary_box']['left'];
          $tickRight    = $this->calculated['boundary_box']['left'] + $this->parameter['tick_length'];
        } else {
          $tickLeft     = $this->calculated['boundary_box']['left'] + $this->parameter['tick_length'];
          $tickRight    = $this->calculated['boundary_box']['left'];
        }
        $textRight      = $tickLeft - $this->calculated['left_inner_padding'];

        if ($axis_angle == 0)  $reference = 'right-center';
        if ($axis_angle > 0)   $reference = 'right-top';
        if ($axis_angle < 0)   $reference = 'right-bottom';
        if ($axis_angle == 90) $reference = 'right-center';

        foreach ($this->calculated['y_axis']['tick_y'] as $set => $tickY) {
          $tickY = (int) round($tickY);
          // draw y grid if colour specified
          if ($yGrid != 'none') {
            switch ($yGrid) {
              case 'line':
                ImageLine($this->image, $gridLeft, $tickY, $gridRight, $tickY, $gridColour);
                break;
               case 'dash':
                $this->image_dashed_line($this->image, $gridLeft, $tickY, $gridRight, $tickY, $gridColour); // Moodle
                break;
            }
          }

          // y axis text
          if ($this->parameter['y_axis_text_left'] && !($set % $this->parameter['y_axis_text_left'])) { // test if tick should be displayed
            // draw tick
            if ($tickColour != 'none')
              ImageLine($this->image, $tickLeft, $tickY, $tickRight, $tickY, $tickColour);

            // draw axis text...
            $coords = array('x' => $textRight, 'y' => $tickY, 'reference' => $reference);
            $axisTag['text'] = $this->calculated['y_axis_left']['text'][$set];
            $axisTag['boundary_box'] = $this->calculated['y_axis_left']['boundary_box'][$set];
            $this->update_boundaryBox($axisTag['boundary_box'], $coords);
            $this->print_TTF($axisTag);
          }
        }
      }

      if ($this->calculated['y_axis_right']['has_data']) {
        // RIGHT HAND SIDE
        // left and right coords for ticks
        if ($this->parameter['tick_length'] >= 0) {
          $tickLeft     = $this->calculated['boundary_box']['right'] - $this->parameter['tick_length'];
          $tickRight    = $this->calculated['boundary_box']['right'];
        } else {
          $tickLeft     = $this->calculated['boundary_box']['right'];
          $tickRight    = $this->calculated['boundary_box']['right'] - $this->parameter['tick_length'];
        }
        $textLeft       = $tickRight+ $this->calculated['left_inner_padding'];

        if ($axis_angle == 0)  $reference = 'left-center';
        if ($axis_angle > 0)   $reference = 'left-bottom';
        if ($axis_angle < 0)   $reference = 'left-top';
        if ($axis_angle == 90) $reference = 'left-center';

        foreach ($this->calculated['y_axis']['tick_y'] as $set => $tickY) {
          if (!$this->calculated['y_axis_left']['has_data'] && $yGrid != 'none') { // draw grid if not drawn already (above)
            switch ($yGrid) {
              case 'line':
                ImageLine($this->image, (int) round($gridLeft), (int) round($tickY), (int) round($gridRight), (int) round($tickY), $gridColour);
                break;
               case 'dash':
                $this->image_dashed_line($this->image, (int) round($gridLeft), (int) round($tickY), (int) round($gridRight), (int) round($tickY), $gridColour); // Moodle
                break;
            }
          }

          if ($this->parameter['y_axis_text_right'] && !($set % $this->parameter['y_axis_text_right'])) { // test if tick should be displayed
            // draw tick
            if ($tickColour != 'none')
              ImageLine($this->image, (int) round($tickLeft), (int) round($tickY), (int) round($tickRight), (int) round($tickY), $tickColour);

            // draw axis text...
            $coords = array('x' => $textLeft, 'y' => $tickY, 'reference' => $reference);
            $axisTag['text'] = $this->calculated['y_axis_right']['text'][$set];
            $axisTag['boundary_box'] = $this->calculated['y_axis_left']['boundary_box'][$set];
            $this->update_boundaryBox($axisTag['boundary_box'], $coords);
            $this->print_TTF($axisTag);
          }
        }
      }
    }

    function init_data() {
      $this->calculated['y_plot'] = array(); // array to hold pixel plotting coords for y axis
      $height = $this->calculated['boundary_box']['bottom'] - $this->calculated['boundary_box']['top'];
      $width  = $this->calculated['boundary_box']['right'] - $this->calculated['boundary_box']['left'];

      // calculate pixel steps between axis ticks.
      $this->calculated['y_axis']['step'] = $height / ($this->parameter['y_axis_gridlines'] - 1);

      // calculate x ticks spacing taking into account x offset for ticks.
      $extraTick  = 2 * $this->parameter['x_offset']; // extra tick to account for padding
      $numTicks = $this->calculated['x_axis']['num_ticks'] - 1;    // number of x ticks

      // Hack by rodger to avoid division by zero, see bug 1231
      if ($numTicks==0) $numTicks=1;

      $this->calculated['x_axis']['step'] = $width / ($numTicks + $extraTick);
      $widthPlot = $width - ($this->calculated['x_axis']['step'] * $extraTick);
      $this->calculated['x_axis']['step'] = $widthPlot / $numTicks;

      //calculate factor for transforming x,y physical coords to logical coords for right hand y_axis.
      $y_range = $this->calculated['y_axis_right']['max'] - $this->calculated['y_axis_right']['min'];
      $y_range = ($y_range ? $y_range : 1);
      $this->calculated['y_axis_right']['factor'] = $height / $y_range;

      //calculate factor for transforming x,y physical coords to logical coords for left hand axis.
      $yRange = $this->calculated['y_axis_left']['max'] - $this->calculated['y_axis_left']['min'];
      $yRange = ($yRange ? $yRange : 1);
      $this->calculated['y_axis_left']['factor'] = $height / $yRange;
      if ($this->parameter['x_axis_gridlines'] != 'auto') {
        $xRange = $this->calculated['x_axis']['max'] - $this->calculated['x_axis']['min'];
        $xRange = ($xRange ? $xRange : 1);
        $this->calculated['x_axis']['factor'] = $widthPlot / $xRange;
      }

      //expand_pre($this->calculated['boundary_box']);
      // cycle thru all data sets...
      $this->calculated['num_bars'] = 0;
      foreach ($this->y_order as $order => $set) {
        // determine how many bars there are
        if (isset($this->y_format[$set]['bar']) && ($this->y_format[$set]['bar'] != 'none')) {
          $this->calculated['bar_offset_index'][$set] = $this->calculated['num_bars']; // index to relate bar with data set.
          $this->calculated['num_bars']++;
        }

        // calculate y coords for plotting data
        foreach ($this->x_data as $index => $x) {
          $this->calculated['y_plot'][$set][$index] = $this->y_data[$set][$index];

          if ((string)$this->y_data[$set][$index] != 'none') {

            if (isset($this->y_format[$set]['y_axis']) && $this->y_format[$set]['y_axis'] == 'right') {
              $this->calculated['y_plot'][$set][$index] =
                (int) round(($this->y_data[$set][$index] - $this->calculated['y_axis_right']['min'])
                  * $this->calculated['y_axis_right']['factor']);
            } else {
              //print "$set $index<br />";
              $this->calculated['y_plot'][$set][$index] =
                (int) round(($this->y_data[$set][$index] - $this->calculated['y_axis_left']['min'])
                  * $this->calculated['y_axis_left']['factor']);
            }

          }
        }
      }
      //print "factor ".$this->calculated['x_axis']['factor']."<br />";
      //expand_pre($this->calculated['x_plot']);

      // calculate bar parameters if bars are to be drawn.
      if ($this->calculated['num_bars']) {
        $xStep       = $this->calculated['x_axis']['step'];
        $totalWidth  = $this->calculated['x_axis']['step'] - $this->parameter['bar_spacing'];
        $barWidth    = $totalWidth / $this->calculated['num_bars'];

        $barX = ($barWidth - $totalWidth) / 2; // starting x offset
        for ($i=0; $i < $this->calculated['num_bars']; $i++) {
          $this->calculated['bar_offset_x'][$i] = $barX;
          $barX += $barWidth; // add width of bar to x offset.
        }
        $this->calculated['bar_width'] = $barWidth;
      }


    }

    function init_x_ticks() {
      // get coords for x axis ticks and data plots
      //$xGrid       = $this->parameter['x_grid'];
      $xStep       = $this->calculated['x_axis']['step'];
      $ticksOffset = $this->parameter['x_offset']; // where to start drawing ticks relative to y axis.
      $gridLeft    = $this->calculated['boundary_box']['left'] + ($xStep * $ticksOffset); // grid x start
      $tickX       = $gridLeft; // tick x coord

      foreach ($this->calculated['x_axis']['text'] as $set => $value) {
        //print "index: $set<br />";
        // x tick value
        $this->calculated['x_axis']['tick_x'][$set] = $tickX;
        // if num ticks is auto then x plot value is same as x  tick
        if ($this->parameter['x_axis_gridlines'] == 'auto') $this->calculated['x_plot'][$set] = (int) round($tickX);
        //print $this->calculated['x_plot'][$set].'<br />';
        $tickX += $xStep;
      }

      //print "xStep: $xStep <br />";
      // if numeric x axis then calculate x coords for each data point. this is seperate from x ticks.
      $gridX = $gridLeft;
      if (empty($this->calculated['x_axis']['factor'])) {
          $this->calculated['x_axis']['factor'] = 0;
      }
      if (empty($this->calculated['x_axis']['min'])) {
          $this->calculated['x_axis']['min'] = 0;
      }
      $factor = $this->calculated['x_axis']['factor'];
      $min = $this->calculated['x_axis']['min'];

      if ($this->parameter['x_axis_gridlines'] != 'auto') {
        foreach ($this->x_data as $index => $x) {
          //print "index: $index, x: $x<br />";
          $offset = $x - $this->calculated['x_axis']['min'];

          //$gridX = ($offset * $this->calculated['x_axis']['factor']);
          //print "offset: $offset <br />";
          //$this->calculated['x_plot'][$set] = $gridLeft + ($offset * $this->calculated['x_axis']['factor']);

          $this->calculated['x_plot'][$index] = $gridLeft + ($x - $min) * $factor;

          //print $this->calculated['x_plot'][$set].'<br />';
        }
      }
      //expand_pre($this->calculated['boundary_box']);
      //print "factor ".$this->calculated['x_axis']['factor']."<br />";
      //expand_pre($this->calculated['x_plot']);
    }

    function init_y_ticks() {
      // get coords for y axis ticks

      $yStep      = $this->calculated['y_axis']['step'];
      $gridBottom = $this->calculated['boundary_box']['bottom'];
      $tickY      = $gridBottom; // tick y coord

      for ($i = 0; $i < $this->parameter['y_axis_gridlines']; $i++) {
        $this->calculated['y_axis']['tick_y'][$i] = $tickY;
        $tickY   -= $yStep;
      }

    }

    function init_labels() {
      if ($this->parameter['title']) {
        $size = $this->get_boundaryBox(
          array('points' => $this->parameter['title_size'],
                'angle'  => 0,
                'font'   => $this->parameter['title_font'],
                'text'   => $this->parameter['title']));
        $this->calculated['title']['boundary_box']  = $size;
        $this->calculated['title']['text']         = $this->parameter['title'];
        $this->calculated['title']['font']         = $this->parameter['title_font'];
        $this->calculated['title']['points']       = $this->parameter['title_size'];
        $this->calculated['title']['colour']       = $this->parameter['title_colour'];
        $this->calculated['title']['angle']        = 0;

        $this->calculated['boundary_box']['top'] += $size['height'] + $this->parameter['outer_padding'];
        //$this->calculated['boundary_box']['top'] += $size['height'];

      } else $this->calculated['title']['boundary_box'] = $this->get_null_size();

      if ($this->parameter['y_label_left']) {
        $this->calculated['y_label_left']['text']    = $this->parameter['y_label_left'];
        $this->calculated['y_label_left']['angle']   = $this->parameter['y_label_angle'];
        $this->calculated['y_label_left']['font']    = $this->parameter['label_font'];
        $this->calculated['y_label_left']['points']  = $this->parameter['label_size'];
        $this->calculated['y_label_left']['colour']  = $this->parameter['label_colour'];

        $size = $this->get_boundaryBox($this->calculated['y_label_left']);
        $this->calculated['y_label_left']['boundary_box']  = $size;
        //$this->calculated['boundary_box']['left'] += $size['width'] + $this->parameter['inner_padding'];
        $this->calculated['boundary_box']['left'] += $size['width'];

      } else $this->calculated['y_label_left']['boundary_box'] = $this->get_null_size();

      if ($this->parameter['y_label_right']) {
        $this->calculated['y_label_right']['text']    = $this->parameter['y_label_right'];
        $this->calculated['y_label_right']['angle']   = $this->parameter['y_label_angle'];
        $this->calculated['y_label_right']['font']    = $this->parameter['label_font'];
        $this->calculated['y_label_right']['points']  = $this->parameter['label_size'];
        $this->calculated['y_label_right']['colour']  = $this->parameter['label_colour'];

        $size = $this->get_boundaryBox($this->calculated['y_label_right']);
        $this->calculated['y_label_right']['boundary_box']  = $size;
        //$this->calculated['boundary_box']['right'] -= $size['width'] + $this->parameter['inner_padding'];
        $this->calculated['boundary_box']['right'] -= $size['width'];

      } else $this->calculated['y_label_right']['boundary_box'] = $this->get_null_size();

      if ($this->parameter['x_label']) {
        $this->calculated['x_label']['text']         = $this->parameter['x_label'];
        $this->calculated['x_label']['angle']        = $this->parameter['x_label_angle'];
        $this->calculated['x_label']['font']         = $this->parameter['label_font'];
        $this->calculated['x_label']['points']       = $this->parameter['label_size'];
        $this->calculated['x_label']['colour']       = $this->parameter['label_colour'];

        $size = $this->get_boundaryBox($this->calculated['x_label']);
        $this->calculated['x_label']['boundary_box']  = $size;
        //$this->calculated['boundary_box']['bottom'] -= $size['height'] + $this->parameter['inner_padding'];
        $this->calculated['boundary_box']['bottom'] -= $size['height'];

      } else $this->calculated['x_label']['boundary_box'] = $this->get_null_size();

    }


    function init_legend() {
      $this->calculated['legend'] = array(); // array to hold calculated values for legend.
      //$this->calculated['legend']['boundary_box_max'] = array('height' => 0, 'width' => 0);
      $this->calculated['legend']['boundary_box_max'] = $this->get_null_size();
      if ($this->parameter['legend'] == 'none') return;

      $position = $this->parameter['legend'];
      $numSets = 0; // number of data sets with legends.
      $sumTextHeight = 0; // total of height of all legend text items.
      $width = 0;
      $height = 0;

      foreach ($this->y_order as $set) {
       $text = isset($this->y_format[$set]['legend']) ? $this->y_format[$set]['legend'] : 'none';
       $size = $this->get_boundaryBox(
         array('points' => $this->parameter['legend_size'],
               'angle'  => 0,
               'font'   => $this->parameter['legend_font'],
               'text'   => $text));

       $this->calculated['legend']['boundary_box'][$set] = $size;
       $this->calculated['legend']['text'][$set]        = $text;
       //$this->calculated['legend']['font'][$set]        = $this->parameter['legend_font'];
       //$this->calculated['legend']['points'][$set]      = $this->parameter['legend_size'];
       //$this->calculated['legend']['angle'][$set]       = 0;

       if ($text && $text!='none') {
         $numSets++;
         $sumTextHeight += $size['height'];
       }

       if ($size['width'] > $this->calculated['legend']['boundary_box_max']['width'])
         $this->calculated['legend']['boundary_box_max'] = $size;
      }

      $offset  = $this->parameter['legend_offset'];  // offset in pixels of legend box from graph border.
      $padding = $this->parameter['legend_padding']; // padding in pixels around legend text.
      $textWidth = $this->calculated['legend']['boundary_box_max']['width']; // width of largest legend item.
      $textHeight = $this->calculated['legend']['boundary_box_max']['height']; // use height as size to use for colour square in legend.
      $width = $padding * 2 + $textWidth + $textHeight * 2;  // left and right padding + maximum text width + space for square
      $height = ($padding + $textHeight) * $numSets + $padding; // top and bottom padding + padding between text + text.

      $this->calculated['legend']['boundary_box_all'] = array('width'     => $width,
                                                            'height'    => $height,
                                                            'offset'    => $offset,
                                                            'reference' => $position);

      switch ($position) { // move in right or bottom if legend is outside data plotting area.
        case 'outside-top' :
          $this->calculated['boundary_box']['right']      -= $offset + $width; // move in right hand side
          break;

        case 'outside-bottom' :
          $this->calculated['boundary_box']['right']      -= $offset + $width; // move in right hand side
          break;

        case 'outside-left' :
          $this->calculated['boundary_box']['bottom']      -= $offset + $height; // move in right hand side
          break;

        case 'outside-right' :
          $this->calculated['boundary_box']['bottom']      -= $offset + $height; // move in right hand side
          break;
      }
    }

    function init_y_axis() {
      $this->calculated['y_axis_left'] = array(); // array to hold calculated values for y_axis on left.
      $this->calculated['y_axis_left']['boundary_box_max'] = $this->get_null_size();
      $this->calculated['y_axis_right'] = array(); // array to hold calculated values for y_axis on right.
      $this->calculated['y_axis_right']['boundary_box_max'] = $this->get_null_size();

      $axis_font       = $this->parameter['axis_font'];
      $axis_size       = $this->parameter['axis_size'];
      $axis_colour     = $this->parameter['axis_colour'];
      $axis_angle      = $this->parameter['y_axis_angle'];
      $y_tick_labels   = $this->y_tick_labels;

      $this->calculated['y_axis_left']['has_data'] = FALSE;
      $this->calculated['y_axis_right']['has_data'] = FALSE;

      // find min and max y values.
      $minLeft = $this->parameter['y_min_left'];
      $maxLeft = $this->parameter['y_max_left'];
      $minRight = $this->parameter['y_min_right'];
      $maxRight = $this->parameter['y_max_right'];
      $dataLeft = array();
      $dataRight = array();
      foreach ($this->y_order as $order => $set) {
        if (isset($this->y_format[$set]['y_axis']) && $this->y_format[$set]['y_axis'] == 'right') {
          $this->calculated['y_axis_right']['has_data'] = TRUE;
          $dataRight = array_merge($dataRight, $this->y_data[$set]);
        } else {
          $this->calculated['y_axis_left']['has_data'] = TRUE;
          $dataLeft = array_merge($dataLeft, $this->y_data[$set]);
        }
      }
      $dataLeftRange = $this->find_range($dataLeft, $minLeft, $maxLeft, $this->parameter['y_resolution_left']);
      $dataRightRange = $this->find_range($dataRight, $minRight, $maxRight, $this->parameter['y_resolution_right']);
      $minLeft = $dataLeftRange['min'];
      $maxLeft = $dataLeftRange['max'];
      $minRight = $dataRightRange['min'];
      $maxRight = $dataRightRange['max'];

      $this->calculated['y_axis_left']['min']  = $minLeft;
      $this->calculated['y_axis_left']['max']  = $maxLeft;
      $this->calculated['y_axis_right']['min'] = $minRight;
      $this->calculated['y_axis_right']['max'] = $maxRight;

      $stepLeft = ($maxLeft - $minLeft) / ($this->parameter['y_axis_gridlines'] - 1);
      $startLeft = $minLeft;
      $step_right = ($maxRight - $minRight) / ($this->parameter['y_axis_gridlines'] - 1);
      $start_right = $minRight;

      if ($this->parameter['y_axis_text_left']) {
        for ($i = 0; $i < $this->parameter['y_axis_gridlines']; $i++) { // calculate y axis text sizes
          // left y axis
          if ($y_tick_labels) {
            $value = $y_tick_labels[$i];
          } else {
            $value = number_format($startLeft, $this->parameter['y_decimal_left'], $this->parameter['decimal_point'], $this->parameter['thousand_sep']);
          }
          $this->calculated['y_axis_left']['data'][$i]  = $startLeft;
          $this->calculated['y_axis_left']['text'][$i]  = $value; // text is formatted raw data

          $size = $this->get_boundaryBox(
            array('points' => $axis_size,
                  'font'   => $axis_font,
                  'angle'  => $axis_angle,
                  'colour' => $axis_colour,
                  'text'   => $value));
          $this->calculated['y_axis_left']['boundary_box'][$i] = $size;

          if ($size['height'] > $this->calculated['y_axis_left']['boundary_box_max']['height'])
            $this->calculated['y_axis_left']['boundary_box_max']['height'] = $size['height'];
          if ($size['width'] > $this->calculated['y_axis_left']['boundary_box_max']['width'])
            $this->calculated['y_axis_left']['boundary_box_max']['width'] = $size['width'];

          $startLeft += $stepLeft;
        }
        $this->calculated['boundary_box']['left'] += $this->calculated['y_axis_left']['boundary_box_max']['width']
                                                    + $this->parameter['y_inner_padding'];
      }

      if ($this->parameter['y_axis_text_right']) {
        for ($i = 0; $i < $this->parameter['y_axis_gridlines']; $i++) { // calculate y axis text sizes
          // right y axis
          $value = number_format($start_right, $this->parameter['y_decimal_right'], $this->parameter['decimal_point'], $this->parameter['thousand_sep']);
          $this->calculated['y_axis_right']['data'][$i]  = $start_right;
          $this->calculated['y_axis_right']['text'][$i]  = $value; // text is formatted raw data
          $size = $this->get_boundaryBox(
            array('points' => $axis_size,
                  'font'   => $axis_font,
                  'angle'  => $axis_angle,
                  'colour' => $axis_colour,
                  'text'   => $value));
          $this->calculated['y_axis_right']['boundary_box'][$i] = $size;

          if ($size['height'] > $this->calculated['y_axis_right']['boundary_box_max']['height'])
            $this->calculated['y_axis_right']['boundary_box_max'] = $size;
          if ($size['width'] > $this->calculated['y_axis_right']['boundary_box_max']['width'])
            $this->calculated['y_axis_right']['boundary_box_max']['width'] = $size['width'];

          $start_right += $step_right;
        }
        $this->calculated['boundary_box']['right'] -= $this->calculated['y_axis_right']['boundary_box_max']['width']
                                                    + $this->parameter['y_inner_padding'];
      }
    }

    function init_x_axis() {
      $this->calculated['x_axis'] = array(); // array to hold calculated values for x_axis.
      $this->calculated['x_axis']['boundary_box_max'] = array('height' => 0, 'width' => 0);

      $axis_font       = $this->parameter['axis_font'];
      $axis_size       = $this->parameter['axis_size'];
      $axis_colour     = $this->parameter['axis_colour'];
      $axis_angle      = $this->parameter['x_axis_angle'];

      // check whether to treat x axis as numeric
      if ($this->parameter['x_axis_gridlines'] == 'auto') { // auto means text based x_axis, not numeric...
        $this->calculated['x_axis']['num_ticks'] = sizeof($this->x_data);
          $data = $this->x_data;
          for ($i=0; $i < $this->calculated['x_axis']['num_ticks']; $i++) {
            $value = array_shift($data); // grab value from begin of array
            $this->calculated['x_axis']['data'][$i]  = $value;
            $this->calculated['x_axis']['text'][$i]  = $value; // raw data and text are both the same in this case
            $size = $this->get_boundaryBox(
              array('points' => $axis_size,
                    'font'   => $axis_font,
                    'angle'  => $axis_angle,
                    'colour' => $axis_colour,
                    'text'   => $value));
            $this->calculated['x_axis']['boundary_box'][$i] = $size;
            if ($size['height'] > $this->calculated['x_axis']['boundary_box_max']['height'])
              $this->calculated['x_axis']['boundary_box_max'] = $size;
          }

      } else { // x axis is numeric so find max min values...
        $this->calculated['x_axis']['num_ticks'] = $this->parameter['x_axis_gridlines'];

        $min = $this->parameter['x_min'];
        $max = $this->parameter['x_max'];
        $data = array();
        $data = $this->find_range($this->x_data, $min, $max, $this->parameter['x_resolution']);
        $min = $data['min'];
        $max = $data['max'];
        $this->calculated['x_axis']['min'] = $min;
        $this->calculated['x_axis']['max'] = $max;

        $step = ($max - $min) / ($this->calculated['x_axis']['num_ticks'] - 1);
        $start = $min;

        for ($i = 0; $i < $this->calculated['x_axis']['num_ticks']; $i++) { // calculate x axis text sizes
          $value = number_format($start, $this->parameter['xDecimal'], $this->parameter['decimal_point'], $this->parameter['thousand_sep']);
          $this->calculated['x_axis']['data'][$i]  = $start;
          $this->calculated['x_axis']['text'][$i]  = $value; // text is formatted raw data

          $size = $this->get_boundaryBox(
            array('points' => $axis_size,
                  'font'   => $axis_font,
                  'angle'  => $axis_angle,
                  'colour' => $axis_colour,
                  'text'   => $value));
          $this->calculated['x_axis']['boundary_box'][$i] = $size;

          if ($size['height'] > $this->calculated['x_axis']['boundary_box_max']['height'])
            $this->calculated['x_axis']['boundary_box_max'] = $size;

          $start += $step;
        }
      }
      if ($this->parameter['x_axis_text'])
        $this->calculated['boundary_box']['bottom'] -= $this->calculated['x_axis']['boundary_box_max']['height']
                                                      + $this->parameter['x_inner_padding'];
    }

    // find max and min values for a data array given the resolution.
    function find_range($data, $min, $max, $resolution) {
      if (sizeof($data) == 0 ) return array('min' => 0, 'max' => 0);
      foreach ($data as $key => $value) {
        if ($value=='none') continue;
        if ($value > $max) $max = $value;
        if ($value < $min) $min = $value;
      }

      if ($max == 0) {
        $factor = 1;
      } else {
        if ($max < 0) $factor = - pow(10, (floor(log10(abs($max))) + $resolution) );
        else $factor = pow(10, (floor(log10(abs($max))) - $resolution) );
      }
      if ($factor > 0.1) { // To avoid some wierd rounding errors (Moodle)
        $factor = (int) round($factor * 1000.0) / 1000.0; // To avoid some wierd rounding errors (Moodle)
      } // To avoid some wierd rounding errors (Moodle)

      $max = $factor * @ceil($max / $factor);
      $min = $factor * @floor($min / $factor);

      //print "max=$max, min=$min<br />";

      return array('min' => $min, 'max' => $max);
    }

    public function __construct() {
      if (func_num_args() == 2) {
        $this->parameter['width']  = func_get_arg(0);
        $this->parameter['height'] = func_get_arg(1);
      }
      //$this->boundaryBox  = array(
      $this->calculated['boundary_box'] = array(
        'left'      =>  0,
        'top'       =>  0,
        'right'     =>  $this->parameter['width'] - 1,
        'bottom'    =>  $this->parameter['height'] - 1);

      $this->init_colours();

      //ImageColorTransparent($this->image, $this->colour['white']); // colour for transparency
    }

    /**
     * Old syntax of class constructor. Deprecated in PHP7.
     *
     * @deprecated since Moodle 3.1
     */
    public function graph() {
        debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
        self::__construct();
    }

    /**
     * Prepare label's text for GD output.
     *
     * @param string    $label string to be prepared.
     * @return string   Reversed input string, if we are in RTL mode and has no numbers.
     *                  Otherwise, returns the string as is.
     */
    private function prepare_label_text($label) {
        if (right_to_left() and !preg_match('/[0-9]/i', $label)) {
            return core_text::strrev($label);
        } else {
            return $label;
        }
    }

    function print_TTF($message) {
      $points    = $message['points'];
      $angle     = $message['angle'];
      // We have to manually reverse the label, since php GD cannot handle RTL characters properly in UTF8 strings.
      $text      = $this->prepare_label_text($message['text']);
      $colour    = $this->colour[$message['colour']];
      $font      = $this->parameter['path_to_fonts'].$message['font'];

      $x         = $message['boundary_box']['x'];
      $y         = $message['boundary_box']['y'];
      $offsetX   = $message['boundary_box']['offsetX'];
      $offsetY   = $message['boundary_box']['offsetY'];
      $height    = $message['boundary_box']['height'];
      $width     = $message['boundary_box']['width'];
      $reference = $message['boundary_box']['reference'];

      switch ($reference) {
        case 'top-left':
        case 'left-top':
          $y += $height - $offsetY;
          //$y += $offsetY;
          $x += $offsetX;
          break;
        case 'left-center':
          $y += ($height / 2) - $offsetY;
          $x += $offsetX;
          break;
        case 'left-bottom':
          $y -= $offsetY;
          $x += $offsetX;
         break;
        case 'top-center':
          $y += $height - $offsetY;
          $x -= ($width / 2) - $offsetX;
         break;
        case 'top-right':
        case 'right-top':
          $y += $height - $offsetY;
          $x -= $width  - $offsetX;
          break;
        case 'right-center':
          $y += ($height / 2) - $offsetY;
          $x -= $width  - $offsetX;
          break;
        case 'right-bottom':
          $y -= $offsetY;
          $x -= $width  - $offsetX;
          break;
        case 'bottom-center':
          $y -= $offsetY;
          $x -= ($width / 2) - $offsetX;
         break;
        default:
          $y = 0;
          $x = 0;
          break;
      }
      // start of Moodle addition
      $text = core_text::utf8_to_entities($text, true, true); //does not work with hex entities!
      // end of Moodle addition
      [$x, $y] = [(int) round($x), (int) round($y)];
      ImageTTFText($this->image, $points, $angle, $x, $y, $colour, $font, $text);
    }

    // move boundaryBox to coordinates specified
    function update_boundaryBox(&$boundaryBox, $coords) {
      $width      = $boundaryBox['width'];
      $height     = $boundaryBox['height'];
      $x          = $coords['x'];
      $y          = $coords['y'];
      $reference  = $coords['reference'];
      switch ($reference) {
        case 'top-left':
        case 'left-top':
          $top    = $y;
          $bottom = $y + $height;
          $left   = $x;
          $right  = $x + $width;
          break;
        case 'left-center':
          $top    = $y - ($height / 2);
          $bottom = $y + ($height / 2);
          $left   = $x;
          $right  = $x + $width;
          break;
        case 'left-bottom':
          $top    = $y - $height;
          $bottom = $y;
          $left   = $x;
          $right  = $x + $width;
          break;
        case 'top-center':
          $top    = $y;
          $bottom = $y + $height;
          $left   = $x - ($width / 2);
          $right  = $x + ($width / 2);
          break;
        case 'right-top':
        case 'top-right':
          $top    = $y;
          $bottom = $y + $height;
          $left   = $x - $width;
          $right  = $x;
          break;
        case 'right-center':
          $top    = $y - ($height / 2);
          $bottom = $y + ($height / 2);
          $left   = $x - $width;
          $right  = $x;
          break;
        case 'bottom=right':
        case 'right-bottom':
          $top    = $y - $height;
          $bottom = $y;
          $left   = $x - $width;
          $right  = $x;
          break;
        default:
          $top    = 0;
          $bottom = $height;
          $left   = 0;
          $right  = $width;
          break;
      }

      $boundaryBox = array_merge($boundaryBox, array('top'       => $top,
                                                     'bottom'    => $bottom,
                                                     'left'      => $left,
                                                     'right'     => $right,
                                                     'x'         => $x,
                                                     'y'         => $y,
                                                     'reference' => $reference));
    }

    function get_null_size() {
      return array('width'      => 0,
                   'height'     => 0,
                   'offsetX'    => 0,
                   'offsetY'    => 0,
                   //'fontHeight' => 0
                   );
    }

    function get_boundaryBox($message) {
      $points  = $message['points'];
      $angle   = $message['angle'];
      $font    = $this->parameter['path_to_fonts'].$message['font'];
      $text    = $message['text'];

      //print ('get_boundaryBox');
      //expandPre($message);

      // get font size
      $bounds = ImageTTFBBox($points, $angle, $font, "W");
      if ($angle < 0) {
        $fontHeight = abs($bounds[7]-$bounds[1]);
      } else if ($angle > 0) {
        $fontHeight = abs($bounds[1]-$bounds[7]);
      } else {
        $fontHeight = abs($bounds[7]-$bounds[1]);
      }

      // get boundary box and offsets for printing at an angle
      // start of Moodle addition
      $text = core_text::utf8_to_entities($text, true, true); //gd does not work with hex entities!
      // end of Moodle addition
      $bounds = ImageTTFBBox($points, $angle, $font, $text);

      if ($angle < 0) {
        $width = abs($bounds[4]-$bounds[0]);
        $height = abs($bounds[3]-$bounds[7]);
        $offsetY = abs($bounds[3]-$bounds[1]);
        $offsetX = 0;

      } else if ($angle > 0) {
        $width = abs($bounds[2]-$bounds[6]);
        $height = abs($bounds[1]-$bounds[5]);
        $offsetY = 0;
        $offsetX = abs($bounds[0]-$bounds[6]);

      } else {
        $width = abs($bounds[4]-$bounds[6]);
        $height = abs($bounds[7]-$bounds[1]);
        $offsetY = $bounds[1];
        $offsetX = 0;
      }

      //return values
      return array('width'      => $width,
                   'height'     => $height,
                   'offsetX'    => $offsetX,
                   'offsetY'    => $offsetY,
                   //'fontHeight' => $fontHeight
                   );
    }

    function draw_rectangle($border, $colour, $type) {
      $colour = $this->colour[$colour];
      switch ($type) {
        case 'fill':    // fill the rectangle
          ImageFilledRectangle($this->image, $border['left'], $border['top'], $border['right'], $border['bottom'], $colour);
          break;
        case 'box':     // all sides
          ImageRectangle($this->image, $border['left'], $border['top'], $border['right'], $border['bottom'], $colour);
          break;
        case 'axis':    // bottom x axis and left y axis
          ImageLine($this->image, $border['left'], $border['top'], $border['left'], $border['bottom'], $colour);
          ImageLine($this->image, $border['left'], $border['bottom'], $border['right'], $border['bottom'], $colour);
          break;
        case 'y':       // left y axis only
        case 'y-left':
          ImageLine($this->image, $border['left'], $border['top'], $border['left'], $border['bottom'], $colour);
          break;
        case 'y-right': // right y axis only
          ImageLine($this->image, $border['right'], $border['top'], $border['right'], $border['bottom'], $colour);
          break;
        case 'x':       // bottom x axis only
          ImageLine($this->image, $border['left'], $border['bottom'], $border['right'], $border['bottom'], $colour);
          break;
        case 'u':       // u shaped. bottom x axis and both left and right y axis.
          ImageLine($this->image, $border['left'], $border['top'], $border['left'], $border['bottom'], $colour);
          ImageLine($this->image, $border['right'], $border['top'], $border['right'], $border['bottom'], $colour);
          ImageLine($this->image, $border['left'], $border['bottom'], $border['right'], $border['bottom'], $colour);
          break;

      }
    }

    function init_colours() {
      $this->image              = ImageCreate($this->parameter['width'], $this->parameter['height']);
      // standard colours
      $this->colour['white']    = ImageColorAllocate ($this->image, 0xFF, 0xFF, 0xFF); // first colour is background colour.
      $this->colour['black']    = ImageColorAllocate ($this->image, 0x00, 0x00, 0x00);
      $this->colour['maroon']   = ImageColorAllocate ($this->image, 0x80, 0x00, 0x00);
      $this->colour['green']    = ImageColorAllocate ($this->image, 0x00, 0x80, 0x00);
      $this->colour['ltgreen']  = ImageColorAllocate ($this->image, 0x52, 0xF1, 0x7F);
      $this->colour['ltltgreen']= ImageColorAllocate ($this->image, 0x99, 0xFF, 0x99);
      $this->colour['olive']    = ImageColorAllocate ($this->image, 0x80, 0x80, 0x00);
      $this->colour['navy']     = ImageColorAllocate ($this->image, 0x00, 0x00, 0x80);
      $this->colour['purple']   = ImageColorAllocate ($this->image, 0x80, 0x00, 0x80);
      $this->colour['gray']     = ImageColorAllocate ($this->image, 0x80, 0x80, 0x80);
      $this->colour['red']      = ImageColorAllocate ($this->image, 0xFF, 0x00, 0x00);
      $this->colour['ltred']    = ImageColorAllocate ($this->image, 0xFF, 0x99, 0x99);
      $this->colour['ltltred']  = ImageColorAllocate ($this->image, 0xFF, 0xCC, 0xCC);
      $this->colour['orange']   = ImageColorAllocate ($this->image, 0xFF, 0x66, 0x00);
      $this->colour['ltorange']   = ImageColorAllocate ($this->image, 0xFF, 0x99, 0x66);
      $this->colour['ltltorange'] = ImageColorAllocate ($this->image, 0xFF, 0xcc, 0x99);
      $this->colour['lime']     = ImageColorAllocate ($this->image, 0x00, 0xFF, 0x00);
      $this->colour['yellow']   = ImageColorAllocate ($this->image, 0xFF, 0xFF, 0x00);
      $this->colour['blue']     = ImageColorAllocate ($this->image, 0x00, 0x00, 0xFF);
      $this->colour['ltblue']   = ImageColorAllocate ($this->image, 0x00, 0xCC, 0xFF);
      $this->colour['ltltblue'] = ImageColorAllocate ($this->image, 0x99, 0xFF, 0xFF);
      $this->colour['fuchsia']  = ImageColorAllocate ($this->image, 0xFF, 0x00, 0xFF);
      $this->colour['aqua']     = ImageColorAllocate ($this->image, 0x00, 0xFF, 0xFF);
      //$this->colour['white']    = ImageColorAllocate ($this->image, 0xFF, 0xFF, 0xFF);
      // shades of gray
      $this->colour['grayF0']   = ImageColorAllocate ($this->image, 0xF0, 0xF0, 0xF0);
      $this->colour['grayEE']   = ImageColorAllocate ($this->image, 0xEE, 0xEE, 0xEE);
      $this->colour['grayDD']   = ImageColorAllocate ($this->image, 0xDD, 0xDD, 0xDD);
      $this->colour['grayCC']   = ImageColorAllocate ($this->image, 0xCC, 0xCC, 0xCC);
      $this->colour['gray33']   = ImageColorAllocate ($this->image, 0x33, 0x33, 0x33);
      $this->colour['gray66']   = ImageColorAllocate ($this->image, 0x66, 0x66, 0x66);
      $this->colour['gray99']   = ImageColorAllocate ($this->image, 0x99, 0x99, 0x99);

      $this->colour['none']   = 'none';
      return true;
    }

    function output() {
      if ($this->debug) { // for debugging purposes.
        //expandPre($this->graph);
        //expandPre($this->y_data);
        //expandPre($this->x_data);
        //expandPre($this->parameter);
      } else {

        $expiresSeconds = $this->parameter['seconds_to_live'];
        $expiresHours = $this->parameter['hours_to_live'];

        if ($expiresHours || $expiresSeconds) {
          $now = mktime (date("H"),date("i"),date("s"),date("m"),date("d"),date("Y"));
          $expires = mktime (date("H")+$expiresHours,date("i"),date("s")+$expiresSeconds,date("m"),date("d"),date("Y"));
          $expiresGMT = gmdate('D, d M Y H:i:s', $expires).' GMT';
          $lastModifiedGMT  = gmdate('D, d M Y H:i:s', $now).' GMT';

          Header('Last-modified: '.$lastModifiedGMT);
          Header('Expires: '.$expiresGMT);
        }

        if ($this->parameter['file_name'] == 'none') {
          switch ($this->parameter['output_format']) {
            case 'GIF':
              Header("Content-type: image/gif");  // GIF??. switch to PNG guys!!
              ImageGIF($this->image);
              break;
            case 'JPEG':
              Header("Content-type: image/jpeg"); // JPEG for line art??. included for completeness.
              ImageJPEG($this->image);
              break;
           default:
              Header("Content-type: image/png");  // preferred output format
              ImagePNG($this->image);
              break;
          }
        } else {
           switch ($this->parameter['output_format']) {
            case 'GIF':
              ImageGIF($this->image, $this->parameter['file_name'].'.gif');
              break;
            case 'JPEG':
              ImageJPEG($this->image, $this->parameter['file_name'].'.jpg');
              break;
           default:
              ImagePNG($this->image, $this->parameter['file_name'].'.png');
              break;
          }
        }

        ImageDestroy($this->image);
      }
    } // function output

    function init_variable(&$variable, $value, $default) {
      if (!empty($value)) $variable = $value;
      else if (isset($default)) $variable = $default;
      else unset($variable);
    }

    // plot a point. options include square, circle, diamond, triangle, and dot. offset is used for drawing shadows.
    // for diamonds and triangles the size should be an even number to get nice look. if odd the points are crooked.
    function plot($x, $y, $type, $size, $colour, $offset) {
      //print("drawing point of type: $type, at offset: $offset");
      $u = $x + $offset;
      $v = $this->calculated['inner_border']['bottom'] - $y + $offset;
      $half = $size / 2;
      [$u, $v, $half] = [(int) round($u), (int) round($v), (int) round($half)];
      switch ($type) {
        case 'square':
          ImageFilledRectangle($this->image, $u-$half, $v-$half, $u+$half, $v+$half, $this->colour[$colour]);
          break;
        case 'square-open':
          ImageRectangle($this->image, $u-$half, $v-$half, $u+$half, $v+$half, $this->colour[$colour]);
          break;
        case 'circle':
          ImageArc($this->image, $u, $v, $size, $size, 0, 360, $this->colour[$colour]);
          ImageFillToBorder($this->image, $u, $v, $this->colour[$colour], $this->colour[$colour]);
          break;
        case 'circle-open':
          ImageArc($this->image, $u, $v, $size, $size, 0, 360, $this->colour[$colour]);
          break;
        case 'diamond':
          if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
            ImageFilledPolygon($this->image, array($u, $v - $half, $u + $half, $v, $u, $v + $half, $u - $half, $v), $this->colour[$colour]);
          } else {
            ImageFilledPolygon($this->image, array($u, $v - $half, $u + $half, $v, $u, $v + $half, $u - $half, $v), 4, $this->colour[$colour]);
          }
          break;
        case 'diamond-open':
          if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
            ImagePolygon($this->image, array($u, $v - $half, $u + $half, $v, $u, $v + $half, $u - $half, $v), $this->colour[$colour]);
          } else {
            ImagePolygon($this->image, array($u, $v - $half, $u + $half, $v, $u, $v + $half, $u - $half, $v), 4, $this->colour[$colour]);
          }
          break;
        case 'triangle':
          if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
            ImageFilledPolygon($this->image, array($u, $v - $half, $u + $half, $v + $half, $u - $half, $v + $half), $this->colour[$colour]);
          } else {
            ImageFilledPolygon($this->image, array($u, $v - $half, $u + $half, $v + $half, $u - $half, $v + $half), 3, $this->colour[$colour]);
          }
          break;
        case 'triangle-open':
          if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
            ImagePolygon($this->image, array($u, $v - $half, $u + $half, $v + $half, $u - $half, $v + $half), $this->colour[$colour]);
          } else {
            ImagePolygon($this->image, array($u, $v - $half, $u + $half, $v + $half, $u - $half, $v + $half), 3, $this->colour[$colour]);
          }
          break;
        case 'dot':
          ImageSetPixel($this->image, $u, $v, $this->colour[$colour]);
          break;
      }
    }

    function bar($x, $y, $type, $size, $colour, $offset, $index, $yoffset) {
      $index_offset = $this->calculated['bar_offset_index'][$index];
      if ( $yoffset ) {
        $bar_offsetx = 0;
      } else {
        $bar_offsetx = $this->calculated['bar_offset_x'][$index_offset];
      }
      //$this->dbug("drawing bar at offset = $offset : index = $index: bar_offsetx = $bar_offsetx");

      $span = ($this->calculated['bar_width'] * $size) / 2;
      $x_left  = $x + $bar_offsetx - $span;
      $x_right = $x + $bar_offsetx + $span;

      if ($this->parameter['zero_axis'] != 'none') {
        $zero = $this->calculated['zero_axis'];
        if ($this->parameter['shadow_below_axis'] ) $zero  += $offset;
        $u_left  = (int) round($x_left + $offset);
        $u_right = (int) round($x_right + $offset - 1);
        $v       = $this->calculated['boundary_box']['bottom'] - $y + $offset;

        if ($v > $zero) {
          $top = $zero +1;
          $bottom = $v;
        } else {
          $top = $v;
          $bottom = $zero - 1;
        }

        [$top, $bottom]  = [(int) round($top), (int) round($bottom)];

        switch ($type) {
          case 'open':
            if ($v > $zero)
              ImageRectangle($this->image, $u_left, $bottom, $u_right, $bottom, $this->colour[$colour]);
            else
              ImageRectangle($this->image, $u_left, $top, $u_right, $top, $this->colour[$colour]);
            ImageRectangle($this->image, $u_left, $top, $u_left, $bottom, $this->colour[$colour]);
            ImageRectangle($this->image, $u_right, $top, $u_right, $bottom, $this->colour[$colour]);
            break;
          case 'fill':
            ImageFilledRectangle($this->image, $u_left, $top, $u_right, $bottom, $this->colour[$colour]);
            break;
        }

      } else {

        $bottom = $this->calculated['boundary_box']['bottom'];
        if ($this->parameter['shadow_below_axis'] ) $bottom  += $offset;
        if ($this->parameter['inner_border'] != 'none') $bottom -= 1; // 1 pixel above bottom if border is to be drawn.
        $u_left  = (int) round($x_left + $offset);
        $u_right = (int) round($x_right + $offset - 1);
        $v       = $this->calculated['boundary_box']['bottom'] - $y + $offset;

        // Moodle addition, plus the function parameter yoffset
        if ($yoffset) {                                           // Moodle
            $yoffset = $yoffset - (int) round(($bottom - $v) / 2.0);    // Moodle
            $bottom -= $yoffset;                                  // Moodle
            $v      -= $yoffset;                                  // Moodle
        }                                                         // Moodle

        [$v, $bottom] = [(int) round($v), (int) round($bottom)];

        switch ($type) {
          case 'open':
            ImageRectangle($this->image, $u_left, $v, $u_right, $bottom, $this->colour[$colour]);
            break;
          case 'fill':
            ImageFilledRectangle($this->image, $u_left, $v, $u_right, $bottom, $this->colour[$colour]);
            break;
        }
      }
    }

    function area($x_start, $y_start, $x_end, $y_end, $type, $colour, $offset) {
      //dbug("drawing area type: $type, at offset: $offset");
      if ($this->parameter['zero_axis'] != 'none') {
        $bottom = $this->calculated['boundary_box']['bottom'];
        $zero   = $this->calculated['zero_axis'];
        if ($this->parameter['shadow_below_axis'] ) $zero  += $offset;
        $u_start = $x_start + $offset;
        $u_end   = $x_end + $offset;
        $v_start = $bottom - $y_start + $offset;
        $v_end   = $bottom - $y_end + $offset;
        switch ($type) {
          case 'fill':
            // draw it this way 'cos the FilledPolygon routine seems a bit buggy.
            if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
              ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), $this->colour[$colour]);
              ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), $this->colour[$colour]);
            } else {
              ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), 4, $this->colour[$colour]);
              ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), 4, $this->colour[$colour]);
            }
            break;
          case 'open':
            ImageLine($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]);
            ImageLine($this->image, $u_start, $v_start, $u_start, $zero, $this->colour[$colour]);
            ImageLine($this->image, $u_end, $v_end, $u_end, $zero, $this->colour[$colour]);
           break;
        }
      } else {
        $bottom = $this->calculated['boundary_box']['bottom'];
        $u_start = $x_start + $offset;
        $u_end   = $x_end + $offset;
        $v_start = $bottom - $y_start + $offset;
        $v_end   = $bottom - $y_end + $offset;

        if ($this->parameter['shadow_below_axis'] ) $bottom  += $offset;
        if ($this->parameter['inner_border'] != 'none') $bottom -= 1; // 1 pixel above bottom if border is to be drawn.
        switch ($type) {
          case 'fill':
          if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
              ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), $this->colour[$colour]);
            } else {
              ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), 4, $this->colour[$colour]);
            }
           break;
            case 'open':
            if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
              ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), $this->colour[$colour]);
            } else {
              ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), 4, $this->colour[$colour]);
            }
            break;
        }
      }
    }

    function line($x_start, $y_start, $x_end, $y_end, $type, $brush_type, $brush_size, $colour, $offset) {
      //dbug("drawing line of type: $type, at offset: $offset");
      $u_start = (int) round($x_start + $offset);
      $v_start = (int) round($this->calculated['boundary_box']['bottom'] - $y_start + $offset);
      $u_end   = (int) round($x_end + $offset);
      $v_end   = (int) round($this->calculated['boundary_box']['bottom'] - $y_end + $offset);

      switch ($type) {
        case 'brush':
          $this->draw_brush_line($u_start, $v_start, $u_end, $v_end, $brush_size, $brush_type, $colour);
         break;
        case 'line' :
          ImageLine($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]);
          break;
        case 'dash':
          $this->image_dashed_line($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]); // Moodle
          break;
      }
    }

    // function to draw line. would prefer to use gdBrush but this is not supported yet.
    function draw_brush_line($x0, $y0, $x1, $y1, $size, $type, $colour) {
      //$this->dbug("line: $x0, $y0, $x1, $y1");
      $dy = $y1 - $y0;
      $dx = $x1 - $x0;
      $t = 0;
      $watchdog = 1024; // precaution to prevent infinite loops.

      $this->draw_brush($x0, $y0, $size, $type, $colour);
      if (abs($dx) > abs($dy)) { // slope < 1
        //$this->dbug("slope < 1");
        $m = $dy / $dx; // compute slope
        $t += $y0;
        $dx = ($dx < 0) ? -1 : 1;
        $m *= $dx;
        while ((int) round($x0) != (int) round($x1)) {
          if (!$watchdog--) break;
          $x0 += $dx; // step to next x value
          $t += $m;   // add slope to y value
          $y = (int) round($t);
          //$this->dbug("x0=$x0, x1=$x1, y=$y watchdog=$watchdog");
          $this->draw_brush($x0, $y, $size, $type, $colour);

        }
      } else { // slope >= 1
        //$this->dbug("slope >= 1");
        $m = $dx / $dy; // compute slope
        $t += $x0;
        $dy = ($dy < 0) ? -1 : 1;
        $m *= $dy;
        while ((int) round($y0) != (int) round($y1)) {
          if (!$watchdog--) break;
          $y0 += $dy; // step to next y value
          $t += $m;   // add slope to x value
          $x = (int) round($t);
          //$this->dbug("x=$x, y0=$y0, y1=$y1 watchdog=$watchdog");
          $this->draw_brush($x, $y0, $size, $type, $colour);

        }
      }
    }

    function draw_brush($x, $y, $size, $type, $colour) {
      $x = (int) round($x);
      $y = (int) round($y);
      $half = (int) round($size / 2);
      switch ($type) {
        case 'circle':
          ImageArc($this->image, $x, $y, $size, $size, 0, 360, $this->colour[$colour]);
          ImageFillToBorder($this->image, $x, $y, $this->colour[$colour], $this->colour[$colour]);
          break;
        case 'square':
          ImageFilledRectangle($this->image, $x-$half, $y-$half, $x+$half, $y+$half, $this->colour[$colour]);
          break;
        case 'vertical':
          ImageFilledRectangle($this->image, $x, $y-$half, $x+1, $y+$half, $this->colour[$colour]);
          break;
        case 'horizontal':
          ImageFilledRectangle($this->image, $x-$half, $y, $x+$half, $y+1, $this->colour[$colour]);
          break;
        case 'slash':
          if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
            ImageFilledPolygon($this->image, array(
              $x + $half, $y - $half,
              $x + $half + 1, $y - $half,
              $x - $half + 1, $y + $half,
              $x - $half, $y + $half
            ), $this->colour[$colour]);
          } else {
            ImageFilledPolygon($this->image, array(
              $x + $half, $y - $half,
              $x + $half + 1, $y - $half,
              $x - $half + 1, $y + $half,
              $x - $half, $y + $half
            ), 4, $this->colour[$colour]);
          }
          break;
        case 'backslash':
          if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
            ImageFilledPolygon($this->image, array(
              $x - $half, $y - $half,
              $x - $half + 1, $y - $half,
              $x + $half + 1, $y + $half,
              $x + $half, $y + $half
            ), $this->colour[$colour]);
          } else {
            ImageFilledPolygon($this->image, array(
              $x - $half, $y-$half,
              $x - $half + 1, $y - $half,
              $x + $half + 1, $y + $half,
              $x + $half, $y + $half
            ), 4, $this->colour[$colour]);
          }
          break;
        default:
          @eval($type); // user can create own brush script.
      }
    }

    /**
     * Moodle.
     *
     * A replacement for deprecated ImageDashedLine function.
     *
     * @param resource|GdImage $image
     * @param int $x1 — x-coordinate for first point.
     * @param int $y1 — y-coordinate for first point.
     * @param int $x2 — x-coordinate for second point.
     * @param int $y2 — y-coordinate for second point.
     * @param int $color
     * @return void
     */
    private function image_dashed_line($image, $x1, $y1, $x2, $y2, $colour): void {
      // Create a dashed style.
      $style = array(
        $colour,
        $colour,
        $colour,
        $colour,
        IMG_COLOR_TRANSPARENT,
        IMG_COLOR_TRANSPARENT,
        IMG_COLOR_TRANSPARENT,
        IMG_COLOR_TRANSPARENT
      );
      imagesetstyle($image, $style);

      // Apply the dashed style.
      imageline($image, $x1, $y1, $x2, $y2, IMG_COLOR_STYLED);
    }

} // class graph
