Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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