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