Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | 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
1441 ariadna 540
      $this->calculated['zero_axis'] = (int) round(
541
        $this->calculated['boundary_box']['top'] +
542
        ($this->calculated['y_axis_left']['max'] * $this->calculated['y_axis_left']['factor'])
543
      );
1 efrain 544
      ImageLine($this->image, $this->calculated['boundary_box']['left'], $this->calculated['zero_axis'], $this->calculated['boundary_box']['right'], $this->calculated['zero_axis'], $this->colour[$colour]);
545
    }
546
 
547
    function draw_zero_axis_right() {
548
      $colour = $this->parameter['zero_axis'];
549
      if ($colour == 'none') return;
550
      // draw zero axis on right hand side
1441 ariadna 551
      $this->calculated['zero_axis'] = (int) round(
552
        $this->calculated['boundary_box']['top'] +
553
        ($this->calculated['y_axis_right']['max'] * $this->calculated['y_axis_right']['factor'])
554
      );
1 efrain 555
      ImageLine($this->image, $this->calculated['boundary_box']['left'], $this->calculated['zero_axis'], $this->calculated['boundary_box']['right'], $this->calculated['zero_axis'], $this->colour[$colour]);
556
    }
557
 
558
    function draw_x_axis() {
559
      $gridColour  = $this->colour[$this->parameter['grid_colour']];
560
      $tickColour  = $this->colour[$this->parameter['x_ticks_colour']];
561
      $axis_colour  = $this->parameter['axis_colour'];
562
      $xGrid       = $this->parameter['x_grid'];
563
      $gridTop     = (int) round($this->calculated['boundary_box']['top']);
564
      $gridBottom  = (int) round($this->calculated['boundary_box']['bottom']);
565
 
566
      if ($this->parameter['tick_length'] >= 0) {
567
        $tickTop     = $this->calculated['boundary_box']['bottom'] - $this->parameter['tick_length'];
568
        $tickBottom  = $this->calculated['boundary_box']['bottom'];
569
        $textBottom  = $tickBottom + $this->calculated['bottom_inner_padding'];
570
      } else {
571
        $tickTop     = $this->calculated['boundary_box']['bottom'];
572
        $tickBottom  = $this->calculated['boundary_box']['bottom'] - $this->parameter['tick_length'];
573
        $textBottom  = $tickBottom + $this->calculated['bottom_inner_padding'];
574
      }
575
 
576
      $axis_font    = $this->parameter['axis_font'];
577
      $axis_size    = $this->parameter['axis_size'];
578
      $axis_angle   = $this->parameter['x_axis_angle'];
579
 
580
      if ($axis_angle == 0)  $reference = 'top-center';
581
      if ($axis_angle > 0)   $reference = 'top-right';
582
      if ($axis_angle < 0)   $reference = 'top-left';
583
      if ($axis_angle == 90) $reference = 'top-center';
584
 
585
      //generic tag information. applies to all axis text.
586
      $axisTag = array('points' => $axis_size, 'angle' => $axis_angle, 'font' => $axis_font, 'colour' => $axis_colour);
587
 
588
      foreach ($this->calculated['x_axis']['tick_x'] as $set => $tickX) {
589
        $tickX = (int) round($tickX);
590
        // draw x grid if colour specified
591
        if ($xGrid != 'none') {
592
          switch ($xGrid) {
593
            case 'line':
594
              ImageLine($this->image, $tickX, $gridTop, $tickX, $gridBottom, $gridColour);
595
              break;
596
             case 'dash':
597
              $this->image_dashed_line($this->image, $tickX, $gridTop, $tickX, $gridBottom, $gridColour); // Moodle
598
              break;
599
          }
600
        }
601
 
602
        if ($this->parameter['x_axis_text'] && !($set % $this->parameter['x_axis_text'])) { // test if tick should be displayed
603
          // draw tick
604
          if ($tickColour != 'none')
605
            ImageLine($this->image, $tickX, $tickTop, $tickX, $tickBottom, $tickColour);
606
 
607
          // draw axis text
608
          $coords = array('x' => $tickX, 'y' => $textBottom, 'reference' => $reference);
609
          $axisTag['text'] = $this->calculated['x_axis']['text'][$set];
610
          $axisTag['boundary_box'] = $this->calculated['x_axis']['boundary_box'][$set];
611
          $this->update_boundaryBox($axisTag['boundary_box'], $coords);
612
          $this->print_TTF($axisTag);
613
        }
614
      }
615
    }
616
 
617
    function draw_y_axis() {
618
      $gridColour  = $this->colour[$this->parameter['grid_colour']];
619
      $tickColour  = $this->colour[$this->parameter['y_ticks_colour']];
620
      $axis_colour  = $this->parameter['axis_colour'];
621
      $yGrid       = $this->parameter['y_grid'];
622
      $gridLeft    = (int) round($this->calculated['boundary_box']['left']);
623
      $gridRight   = (int) round($this->calculated['boundary_box']['right']);
624
 
625
      // axis font information
626
      $axis_font    = $this->parameter['axis_font'];
627
      $axis_size    = $this->parameter['axis_size'];
628
      $axis_angle   = $this->parameter['y_axis_angle'];
629
      $axisTag = array('points' => $axis_size, 'angle' => $axis_angle, 'font' => $axis_font, 'colour' => $axis_colour);
630
 
631
 
632
      if ($this->calculated['y_axis_left']['has_data']) {
633
        // LEFT HAND SIDE
634
        // left and right coords for ticks
635
        if ($this->parameter['tick_length'] >= 0) {
636
          $tickLeft     = $this->calculated['boundary_box']['left'];
637
          $tickRight    = $this->calculated['boundary_box']['left'] + $this->parameter['tick_length'];
638
        } else {
639
          $tickLeft     = $this->calculated['boundary_box']['left'] + $this->parameter['tick_length'];
640
          $tickRight    = $this->calculated['boundary_box']['left'];
641
        }
642
        $textRight      = $tickLeft - $this->calculated['left_inner_padding'];
643
 
644
        if ($axis_angle == 0)  $reference = 'right-center';
645
        if ($axis_angle > 0)   $reference = 'right-top';
646
        if ($axis_angle < 0)   $reference = 'right-bottom';
647
        if ($axis_angle == 90) $reference = 'right-center';
648
 
649
        foreach ($this->calculated['y_axis']['tick_y'] as $set => $tickY) {
650
          $tickY = (int) round($tickY);
651
          // draw y grid if colour specified
652
          if ($yGrid != 'none') {
653
            switch ($yGrid) {
654
              case 'line':
655
                ImageLine($this->image, $gridLeft, $tickY, $gridRight, $tickY, $gridColour);
656
                break;
657
               case 'dash':
658
                $this->image_dashed_line($this->image, $gridLeft, $tickY, $gridRight, $tickY, $gridColour); // Moodle
659
                break;
660
            }
661
          }
662
 
663
          // y axis text
664
          if ($this->parameter['y_axis_text_left'] && !($set % $this->parameter['y_axis_text_left'])) { // test if tick should be displayed
665
            // draw tick
666
            if ($tickColour != 'none')
667
              ImageLine($this->image, $tickLeft, $tickY, $tickRight, $tickY, $tickColour);
668
 
669
            // draw axis text...
670
            $coords = array('x' => $textRight, 'y' => $tickY, 'reference' => $reference);
671
            $axisTag['text'] = $this->calculated['y_axis_left']['text'][$set];
672
            $axisTag['boundary_box'] = $this->calculated['y_axis_left']['boundary_box'][$set];
673
            $this->update_boundaryBox($axisTag['boundary_box'], $coords);
674
            $this->print_TTF($axisTag);
675
          }
676
        }
677
      }
678
 
679
      if ($this->calculated['y_axis_right']['has_data']) {
680
        // RIGHT HAND SIDE
681
        // left and right coords for ticks
682
        if ($this->parameter['tick_length'] >= 0) {
683
          $tickLeft     = $this->calculated['boundary_box']['right'] - $this->parameter['tick_length'];
684
          $tickRight    = $this->calculated['boundary_box']['right'];
685
        } else {
686
          $tickLeft     = $this->calculated['boundary_box']['right'];
687
          $tickRight    = $this->calculated['boundary_box']['right'] - $this->parameter['tick_length'];
688
        }
689
        $textLeft       = $tickRight+ $this->calculated['left_inner_padding'];
690
 
691
        if ($axis_angle == 0)  $reference = 'left-center';
692
        if ($axis_angle > 0)   $reference = 'left-bottom';
693
        if ($axis_angle < 0)   $reference = 'left-top';
694
        if ($axis_angle == 90) $reference = 'left-center';
695
 
696
        foreach ($this->calculated['y_axis']['tick_y'] as $set => $tickY) {
697
          if (!$this->calculated['y_axis_left']['has_data'] && $yGrid != 'none') { // draw grid if not drawn already (above)
698
            switch ($yGrid) {
699
              case 'line':
1441 ariadna 700
                ImageLine($this->image, (int) round($gridLeft), (int) round($tickY), (int) round($gridRight), (int) round($tickY), $gridColour);
1 efrain 701
                break;
702
               case 'dash':
1441 ariadna 703
                $this->image_dashed_line($this->image, (int) round($gridLeft), (int) round($tickY), (int) round($gridRight), (int) round($tickY), $gridColour); // Moodle
1 efrain 704
                break;
705
            }
706
          }
707
 
708
          if ($this->parameter['y_axis_text_right'] && !($set % $this->parameter['y_axis_text_right'])) { // test if tick should be displayed
709
            // draw tick
710
            if ($tickColour != 'none')
1441 ariadna 711
              ImageLine($this->image, (int) round($tickLeft), (int) round($tickY), (int) round($tickRight), (int) round($tickY), $tickColour);
1 efrain 712
 
713
            // draw axis text...
714
            $coords = array('x' => $textLeft, 'y' => $tickY, 'reference' => $reference);
715
            $axisTag['text'] = $this->calculated['y_axis_right']['text'][$set];
716
            $axisTag['boundary_box'] = $this->calculated['y_axis_left']['boundary_box'][$set];
717
            $this->update_boundaryBox($axisTag['boundary_box'], $coords);
718
            $this->print_TTF($axisTag);
719
          }
720
        }
721
      }
722
    }
723
 
724
    function init_data() {
725
      $this->calculated['y_plot'] = array(); // array to hold pixel plotting coords for y axis
726
      $height = $this->calculated['boundary_box']['bottom'] - $this->calculated['boundary_box']['top'];
727
      $width  = $this->calculated['boundary_box']['right'] - $this->calculated['boundary_box']['left'];
728
 
729
      // calculate pixel steps between axis ticks.
730
      $this->calculated['y_axis']['step'] = $height / ($this->parameter['y_axis_gridlines'] - 1);
731
 
732
      // calculate x ticks spacing taking into account x offset for ticks.
733
      $extraTick  = 2 * $this->parameter['x_offset']; // extra tick to account for padding
734
      $numTicks = $this->calculated['x_axis']['num_ticks'] - 1;    // number of x ticks
735
 
736
      // Hack by rodger to avoid division by zero, see bug 1231
737
      if ($numTicks==0) $numTicks=1;
738
 
739
      $this->calculated['x_axis']['step'] = $width / ($numTicks + $extraTick);
740
      $widthPlot = $width - ($this->calculated['x_axis']['step'] * $extraTick);
741
      $this->calculated['x_axis']['step'] = $widthPlot / $numTicks;
742
 
743
      //calculate factor for transforming x,y physical coords to logical coords for right hand y_axis.
744
      $y_range = $this->calculated['y_axis_right']['max'] - $this->calculated['y_axis_right']['min'];
745
      $y_range = ($y_range ? $y_range : 1);
746
      $this->calculated['y_axis_right']['factor'] = $height / $y_range;
747
 
748
      //calculate factor for transforming x,y physical coords to logical coords for left hand axis.
749
      $yRange = $this->calculated['y_axis_left']['max'] - $this->calculated['y_axis_left']['min'];
750
      $yRange = ($yRange ? $yRange : 1);
751
      $this->calculated['y_axis_left']['factor'] = $height / $yRange;
752
      if ($this->parameter['x_axis_gridlines'] != 'auto') {
753
        $xRange = $this->calculated['x_axis']['max'] - $this->calculated['x_axis']['min'];
754
        $xRange = ($xRange ? $xRange : 1);
755
        $this->calculated['x_axis']['factor'] = $widthPlot / $xRange;
756
      }
757
 
758
      //expand_pre($this->calculated['boundary_box']);
759
      // cycle thru all data sets...
760
      $this->calculated['num_bars'] = 0;
761
      foreach ($this->y_order as $order => $set) {
762
        // determine how many bars there are
763
        if (isset($this->y_format[$set]['bar']) && ($this->y_format[$set]['bar'] != 'none')) {
764
          $this->calculated['bar_offset_index'][$set] = $this->calculated['num_bars']; // index to relate bar with data set.
765
          $this->calculated['num_bars']++;
766
        }
767
 
768
        // calculate y coords for plotting data
769
        foreach ($this->x_data as $index => $x) {
770
          $this->calculated['y_plot'][$set][$index] = $this->y_data[$set][$index];
771
 
772
          if ((string)$this->y_data[$set][$index] != 'none') {
773
 
774
            if (isset($this->y_format[$set]['y_axis']) && $this->y_format[$set]['y_axis'] == 'right') {
775
              $this->calculated['y_plot'][$set][$index] =
1441 ariadna 776
                (int) round(($this->y_data[$set][$index] - $this->calculated['y_axis_right']['min'])
1 efrain 777
                  * $this->calculated['y_axis_right']['factor']);
778
            } else {
779
              //print "$set $index<br />";
780
              $this->calculated['y_plot'][$set][$index] =
1441 ariadna 781
                (int) round(($this->y_data[$set][$index] - $this->calculated['y_axis_left']['min'])
1 efrain 782
                  * $this->calculated['y_axis_left']['factor']);
783
            }
784
 
785
          }
786
        }
787
      }
788
      //print "factor ".$this->calculated['x_axis']['factor']."<br />";
789
      //expand_pre($this->calculated['x_plot']);
790
 
791
      // calculate bar parameters if bars are to be drawn.
792
      if ($this->calculated['num_bars']) {
793
        $xStep       = $this->calculated['x_axis']['step'];
794
        $totalWidth  = $this->calculated['x_axis']['step'] - $this->parameter['bar_spacing'];
795
        $barWidth    = $totalWidth / $this->calculated['num_bars'];
796
 
797
        $barX = ($barWidth - $totalWidth) / 2; // starting x offset
798
        for ($i=0; $i < $this->calculated['num_bars']; $i++) {
799
          $this->calculated['bar_offset_x'][$i] = $barX;
800
          $barX += $barWidth; // add width of bar to x offset.
801
        }
802
        $this->calculated['bar_width'] = $barWidth;
803
      }
804
 
805
 
806
    }
807
 
808
    function init_x_ticks() {
809
      // get coords for x axis ticks and data plots
810
      //$xGrid       = $this->parameter['x_grid'];
811
      $xStep       = $this->calculated['x_axis']['step'];
812
      $ticksOffset = $this->parameter['x_offset']; // where to start drawing ticks relative to y axis.
813
      $gridLeft    = $this->calculated['boundary_box']['left'] + ($xStep * $ticksOffset); // grid x start
814
      $tickX       = $gridLeft; // tick x coord
815
 
816
      foreach ($this->calculated['x_axis']['text'] as $set => $value) {
817
        //print "index: $set<br />";
818
        // x tick value
819
        $this->calculated['x_axis']['tick_x'][$set] = $tickX;
820
        // if num ticks is auto then x plot value is same as x  tick
1441 ariadna 821
        if ($this->parameter['x_axis_gridlines'] == 'auto') $this->calculated['x_plot'][$set] = (int) round($tickX);
1 efrain 822
        //print $this->calculated['x_plot'][$set].'<br />';
823
        $tickX += $xStep;
824
      }
825
 
826
      //print "xStep: $xStep <br />";
827
      // if numeric x axis then calculate x coords for each data point. this is seperate from x ticks.
828
      $gridX = $gridLeft;
829
      if (empty($this->calculated['x_axis']['factor'])) {
830
          $this->calculated['x_axis']['factor'] = 0;
831
      }
832
      if (empty($this->calculated['x_axis']['min'])) {
833
          $this->calculated['x_axis']['min'] = 0;
834
      }
835
      $factor = $this->calculated['x_axis']['factor'];
836
      $min = $this->calculated['x_axis']['min'];
837
 
838
      if ($this->parameter['x_axis_gridlines'] != 'auto') {
839
        foreach ($this->x_data as $index => $x) {
840
          //print "index: $index, x: $x<br />";
841
          $offset = $x - $this->calculated['x_axis']['min'];
842
 
843
          //$gridX = ($offset * $this->calculated['x_axis']['factor']);
844
          //print "offset: $offset <br />";
845
          //$this->calculated['x_plot'][$set] = $gridLeft + ($offset * $this->calculated['x_axis']['factor']);
846
 
847
          $this->calculated['x_plot'][$index] = $gridLeft + ($x - $min) * $factor;
848
 
849
          //print $this->calculated['x_plot'][$set].'<br />';
850
        }
851
      }
852
      //expand_pre($this->calculated['boundary_box']);
853
      //print "factor ".$this->calculated['x_axis']['factor']."<br />";
854
      //expand_pre($this->calculated['x_plot']);
855
    }
856
 
857
    function init_y_ticks() {
858
      // get coords for y axis ticks
859
 
860
      $yStep      = $this->calculated['y_axis']['step'];
861
      $gridBottom = $this->calculated['boundary_box']['bottom'];
862
      $tickY      = $gridBottom; // tick y coord
863
 
864
      for ($i = 0; $i < $this->parameter['y_axis_gridlines']; $i++) {
865
        $this->calculated['y_axis']['tick_y'][$i] = $tickY;
866
        $tickY   -= $yStep;
867
      }
868
 
869
    }
870
 
871
    function init_labels() {
872
      if ($this->parameter['title']) {
873
        $size = $this->get_boundaryBox(
874
          array('points' => $this->parameter['title_size'],
875
                'angle'  => 0,
876
                'font'   => $this->parameter['title_font'],
877
                'text'   => $this->parameter['title']));
878
        $this->calculated['title']['boundary_box']  = $size;
879
        $this->calculated['title']['text']         = $this->parameter['title'];
880
        $this->calculated['title']['font']         = $this->parameter['title_font'];
881
        $this->calculated['title']['points']       = $this->parameter['title_size'];
882
        $this->calculated['title']['colour']       = $this->parameter['title_colour'];
883
        $this->calculated['title']['angle']        = 0;
884
 
885
        $this->calculated['boundary_box']['top'] += $size['height'] + $this->parameter['outer_padding'];
886
        //$this->calculated['boundary_box']['top'] += $size['height'];
887
 
888
      } else $this->calculated['title']['boundary_box'] = $this->get_null_size();
889
 
890
      if ($this->parameter['y_label_left']) {
891
        $this->calculated['y_label_left']['text']    = $this->parameter['y_label_left'];
892
        $this->calculated['y_label_left']['angle']   = $this->parameter['y_label_angle'];
893
        $this->calculated['y_label_left']['font']    = $this->parameter['label_font'];
894
        $this->calculated['y_label_left']['points']  = $this->parameter['label_size'];
895
        $this->calculated['y_label_left']['colour']  = $this->parameter['label_colour'];
896
 
897
        $size = $this->get_boundaryBox($this->calculated['y_label_left']);
898
        $this->calculated['y_label_left']['boundary_box']  = $size;
899
        //$this->calculated['boundary_box']['left'] += $size['width'] + $this->parameter['inner_padding'];
900
        $this->calculated['boundary_box']['left'] += $size['width'];
901
 
902
      } else $this->calculated['y_label_left']['boundary_box'] = $this->get_null_size();
903
 
904
      if ($this->parameter['y_label_right']) {
905
        $this->calculated['y_label_right']['text']    = $this->parameter['y_label_right'];
906
        $this->calculated['y_label_right']['angle']   = $this->parameter['y_label_angle'];
907
        $this->calculated['y_label_right']['font']    = $this->parameter['label_font'];
908
        $this->calculated['y_label_right']['points']  = $this->parameter['label_size'];
909
        $this->calculated['y_label_right']['colour']  = $this->parameter['label_colour'];
910
 
911
        $size = $this->get_boundaryBox($this->calculated['y_label_right']);
912
        $this->calculated['y_label_right']['boundary_box']  = $size;
913
        //$this->calculated['boundary_box']['right'] -= $size['width'] + $this->parameter['inner_padding'];
914
        $this->calculated['boundary_box']['right'] -= $size['width'];
915
 
916
      } else $this->calculated['y_label_right']['boundary_box'] = $this->get_null_size();
917
 
918
      if ($this->parameter['x_label']) {
919
        $this->calculated['x_label']['text']         = $this->parameter['x_label'];
920
        $this->calculated['x_label']['angle']        = $this->parameter['x_label_angle'];
921
        $this->calculated['x_label']['font']         = $this->parameter['label_font'];
922
        $this->calculated['x_label']['points']       = $this->parameter['label_size'];
923
        $this->calculated['x_label']['colour']       = $this->parameter['label_colour'];
924
 
925
        $size = $this->get_boundaryBox($this->calculated['x_label']);
926
        $this->calculated['x_label']['boundary_box']  = $size;
927
        //$this->calculated['boundary_box']['bottom'] -= $size['height'] + $this->parameter['inner_padding'];
928
        $this->calculated['boundary_box']['bottom'] -= $size['height'];
929
 
930
      } else $this->calculated['x_label']['boundary_box'] = $this->get_null_size();
931
 
932
    }
933
 
934
 
935
    function init_legend() {
936
      $this->calculated['legend'] = array(); // array to hold calculated values for legend.
937
      //$this->calculated['legend']['boundary_box_max'] = array('height' => 0, 'width' => 0);
938
      $this->calculated['legend']['boundary_box_max'] = $this->get_null_size();
939
      if ($this->parameter['legend'] == 'none') return;
940
 
941
      $position = $this->parameter['legend'];
942
      $numSets = 0; // number of data sets with legends.
943
      $sumTextHeight = 0; // total of height of all legend text items.
944
      $width = 0;
945
      $height = 0;
946
 
947
      foreach ($this->y_order as $set) {
948
       $text = isset($this->y_format[$set]['legend']) ? $this->y_format[$set]['legend'] : 'none';
949
       $size = $this->get_boundaryBox(
950
         array('points' => $this->parameter['legend_size'],
951
               'angle'  => 0,
952
               'font'   => $this->parameter['legend_font'],
953
               'text'   => $text));
954
 
955
       $this->calculated['legend']['boundary_box'][$set] = $size;
956
       $this->calculated['legend']['text'][$set]        = $text;
957
       //$this->calculated['legend']['font'][$set]        = $this->parameter['legend_font'];
958
       //$this->calculated['legend']['points'][$set]      = $this->parameter['legend_size'];
959
       //$this->calculated['legend']['angle'][$set]       = 0;
960
 
961
       if ($text && $text!='none') {
962
         $numSets++;
963
         $sumTextHeight += $size['height'];
964
       }
965
 
966
       if ($size['width'] > $this->calculated['legend']['boundary_box_max']['width'])
967
         $this->calculated['legend']['boundary_box_max'] = $size;
968
      }
969
 
970
      $offset  = $this->parameter['legend_offset'];  // offset in pixels of legend box from graph border.
971
      $padding = $this->parameter['legend_padding']; // padding in pixels around legend text.
972
      $textWidth = $this->calculated['legend']['boundary_box_max']['width']; // width of largest legend item.
973
      $textHeight = $this->calculated['legend']['boundary_box_max']['height']; // use height as size to use for colour square in legend.
974
      $width = $padding * 2 + $textWidth + $textHeight * 2;  // left and right padding + maximum text width + space for square
975
      $height = ($padding + $textHeight) * $numSets + $padding; // top and bottom padding + padding between text + text.
976
 
977
      $this->calculated['legend']['boundary_box_all'] = array('width'     => $width,
978
                                                            'height'    => $height,
979
                                                            'offset'    => $offset,
980
                                                            'reference' => $position);
981
 
982
      switch ($position) { // move in right or bottom if legend is outside data plotting area.
983
        case 'outside-top' :
984
          $this->calculated['boundary_box']['right']      -= $offset + $width; // move in right hand side
985
          break;
986
 
987
        case 'outside-bottom' :
988
          $this->calculated['boundary_box']['right']      -= $offset + $width; // move in right hand side
989
          break;
990
 
991
        case 'outside-left' :
992
          $this->calculated['boundary_box']['bottom']      -= $offset + $height; // move in right hand side
993
          break;
994
 
995
        case 'outside-right' :
996
          $this->calculated['boundary_box']['bottom']      -= $offset + $height; // move in right hand side
997
          break;
998
      }
999
    }
1000
 
1001
    function init_y_axis() {
1002
      $this->calculated['y_axis_left'] = array(); // array to hold calculated values for y_axis on left.
1003
      $this->calculated['y_axis_left']['boundary_box_max'] = $this->get_null_size();
1004
      $this->calculated['y_axis_right'] = array(); // array to hold calculated values for y_axis on right.
1005
      $this->calculated['y_axis_right']['boundary_box_max'] = $this->get_null_size();
1006
 
1007
      $axis_font       = $this->parameter['axis_font'];
1008
      $axis_size       = $this->parameter['axis_size'];
1009
      $axis_colour     = $this->parameter['axis_colour'];
1010
      $axis_angle      = $this->parameter['y_axis_angle'];
1011
      $y_tick_labels   = $this->y_tick_labels;
1012
 
1013
      $this->calculated['y_axis_left']['has_data'] = FALSE;
1014
      $this->calculated['y_axis_right']['has_data'] = FALSE;
1015
 
1016
      // find min and max y values.
1017
      $minLeft = $this->parameter['y_min_left'];
1018
      $maxLeft = $this->parameter['y_max_left'];
1019
      $minRight = $this->parameter['y_min_right'];
1020
      $maxRight = $this->parameter['y_max_right'];
1021
      $dataLeft = array();
1022
      $dataRight = array();
1023
      foreach ($this->y_order as $order => $set) {
1024
        if (isset($this->y_format[$set]['y_axis']) && $this->y_format[$set]['y_axis'] == 'right') {
1025
          $this->calculated['y_axis_right']['has_data'] = TRUE;
1026
          $dataRight = array_merge($dataRight, $this->y_data[$set]);
1027
        } else {
1028
          $this->calculated['y_axis_left']['has_data'] = TRUE;
1029
          $dataLeft = array_merge($dataLeft, $this->y_data[$set]);
1030
        }
1031
      }
1032
      $dataLeftRange = $this->find_range($dataLeft, $minLeft, $maxLeft, $this->parameter['y_resolution_left']);
1033
      $dataRightRange = $this->find_range($dataRight, $minRight, $maxRight, $this->parameter['y_resolution_right']);
1034
      $minLeft = $dataLeftRange['min'];
1035
      $maxLeft = $dataLeftRange['max'];
1036
      $minRight = $dataRightRange['min'];
1037
      $maxRight = $dataRightRange['max'];
1038
 
1039
      $this->calculated['y_axis_left']['min']  = $minLeft;
1040
      $this->calculated['y_axis_left']['max']  = $maxLeft;
1041
      $this->calculated['y_axis_right']['min'] = $minRight;
1042
      $this->calculated['y_axis_right']['max'] = $maxRight;
1043
 
1044
      $stepLeft = ($maxLeft - $minLeft) / ($this->parameter['y_axis_gridlines'] - 1);
1045
      $startLeft = $minLeft;
1046
      $step_right = ($maxRight - $minRight) / ($this->parameter['y_axis_gridlines'] - 1);
1047
      $start_right = $minRight;
1048
 
1049
      if ($this->parameter['y_axis_text_left']) {
1050
        for ($i = 0; $i < $this->parameter['y_axis_gridlines']; $i++) { // calculate y axis text sizes
1051
          // left y axis
1052
          if ($y_tick_labels) {
1053
            $value = $y_tick_labels[$i];
1054
          } else {
1055
            $value = number_format($startLeft, $this->parameter['y_decimal_left'], $this->parameter['decimal_point'], $this->parameter['thousand_sep']);
1056
          }
1057
          $this->calculated['y_axis_left']['data'][$i]  = $startLeft;
1058
          $this->calculated['y_axis_left']['text'][$i]  = $value; // text is formatted raw data
1059
 
1060
          $size = $this->get_boundaryBox(
1061
            array('points' => $axis_size,
1062
                  'font'   => $axis_font,
1063
                  'angle'  => $axis_angle,
1064
                  'colour' => $axis_colour,
1065
                  'text'   => $value));
1066
          $this->calculated['y_axis_left']['boundary_box'][$i] = $size;
1067
 
1068
          if ($size['height'] > $this->calculated['y_axis_left']['boundary_box_max']['height'])
1069
            $this->calculated['y_axis_left']['boundary_box_max']['height'] = $size['height'];
1070
          if ($size['width'] > $this->calculated['y_axis_left']['boundary_box_max']['width'])
1071
            $this->calculated['y_axis_left']['boundary_box_max']['width'] = $size['width'];
1072
 
1073
          $startLeft += $stepLeft;
1074
        }
1075
        $this->calculated['boundary_box']['left'] += $this->calculated['y_axis_left']['boundary_box_max']['width']
1076
                                                    + $this->parameter['y_inner_padding'];
1077
      }
1078
 
1079
      if ($this->parameter['y_axis_text_right']) {
1080
        for ($i = 0; $i < $this->parameter['y_axis_gridlines']; $i++) { // calculate y axis text sizes
1081
          // right y axis
1082
          $value = number_format($start_right, $this->parameter['y_decimal_right'], $this->parameter['decimal_point'], $this->parameter['thousand_sep']);
1083
          $this->calculated['y_axis_right']['data'][$i]  = $start_right;
1084
          $this->calculated['y_axis_right']['text'][$i]  = $value; // text is formatted raw data
1085
          $size = $this->get_boundaryBox(
1086
            array('points' => $axis_size,
1087
                  'font'   => $axis_font,
1088
                  'angle'  => $axis_angle,
1089
                  'colour' => $axis_colour,
1090
                  'text'   => $value));
1091
          $this->calculated['y_axis_right']['boundary_box'][$i] = $size;
1092
 
1093
          if ($size['height'] > $this->calculated['y_axis_right']['boundary_box_max']['height'])
1094
            $this->calculated['y_axis_right']['boundary_box_max'] = $size;
1095
          if ($size['width'] > $this->calculated['y_axis_right']['boundary_box_max']['width'])
1096
            $this->calculated['y_axis_right']['boundary_box_max']['width'] = $size['width'];
1097
 
1098
          $start_right += $step_right;
1099
        }
1100
        $this->calculated['boundary_box']['right'] -= $this->calculated['y_axis_right']['boundary_box_max']['width']
1101
                                                    + $this->parameter['y_inner_padding'];
1102
      }
1103
    }
1104
 
1105
    function init_x_axis() {
1106
      $this->calculated['x_axis'] = array(); // array to hold calculated values for x_axis.
1107
      $this->calculated['x_axis']['boundary_box_max'] = array('height' => 0, 'width' => 0);
1108
 
1109
      $axis_font       = $this->parameter['axis_font'];
1110
      $axis_size       = $this->parameter['axis_size'];
1111
      $axis_colour     = $this->parameter['axis_colour'];
1112
      $axis_angle      = $this->parameter['x_axis_angle'];
1113
 
1114
      // check whether to treat x axis as numeric
1115
      if ($this->parameter['x_axis_gridlines'] == 'auto') { // auto means text based x_axis, not numeric...
1116
        $this->calculated['x_axis']['num_ticks'] = sizeof($this->x_data);
1117
          $data = $this->x_data;
1118
          for ($i=0; $i < $this->calculated['x_axis']['num_ticks']; $i++) {
1119
            $value = array_shift($data); // grab value from begin of array
1120
            $this->calculated['x_axis']['data'][$i]  = $value;
1121
            $this->calculated['x_axis']['text'][$i]  = $value; // raw data and text are both the same in this case
1122
            $size = $this->get_boundaryBox(
1123
              array('points' => $axis_size,
1124
                    'font'   => $axis_font,
1125
                    'angle'  => $axis_angle,
1126
                    'colour' => $axis_colour,
1127
                    'text'   => $value));
1128
            $this->calculated['x_axis']['boundary_box'][$i] = $size;
1129
            if ($size['height'] > $this->calculated['x_axis']['boundary_box_max']['height'])
1130
              $this->calculated['x_axis']['boundary_box_max'] = $size;
1131
          }
1132
 
1133
      } else { // x axis is numeric so find max min values...
1134
        $this->calculated['x_axis']['num_ticks'] = $this->parameter['x_axis_gridlines'];
1135
 
1136
        $min = $this->parameter['x_min'];
1137
        $max = $this->parameter['x_max'];
1138
        $data = array();
1139
        $data = $this->find_range($this->x_data, $min, $max, $this->parameter['x_resolution']);
1140
        $min = $data['min'];
1141
        $max = $data['max'];
1142
        $this->calculated['x_axis']['min'] = $min;
1143
        $this->calculated['x_axis']['max'] = $max;
1144
 
1145
        $step = ($max - $min) / ($this->calculated['x_axis']['num_ticks'] - 1);
1146
        $start = $min;
1147
 
1148
        for ($i = 0; $i < $this->calculated['x_axis']['num_ticks']; $i++) { // calculate x axis text sizes
1149
          $value = number_format($start, $this->parameter['xDecimal'], $this->parameter['decimal_point'], $this->parameter['thousand_sep']);
1150
          $this->calculated['x_axis']['data'][$i]  = $start;
1151
          $this->calculated['x_axis']['text'][$i]  = $value; // text is formatted raw data
1152
 
1153
          $size = $this->get_boundaryBox(
1154
            array('points' => $axis_size,
1155
                  'font'   => $axis_font,
1156
                  'angle'  => $axis_angle,
1157
                  'colour' => $axis_colour,
1158
                  'text'   => $value));
1159
          $this->calculated['x_axis']['boundary_box'][$i] = $size;
1160
 
1161
          if ($size['height'] > $this->calculated['x_axis']['boundary_box_max']['height'])
1162
            $this->calculated['x_axis']['boundary_box_max'] = $size;
1163
 
1164
          $start += $step;
1165
        }
1166
      }
1167
      if ($this->parameter['x_axis_text'])
1168
        $this->calculated['boundary_box']['bottom'] -= $this->calculated['x_axis']['boundary_box_max']['height']
1169
                                                      + $this->parameter['x_inner_padding'];
1170
    }
1171
 
1172
    // find max and min values for a data array given the resolution.
1173
    function find_range($data, $min, $max, $resolution) {
1174
      if (sizeof($data) == 0 ) return array('min' => 0, 'max' => 0);
1175
      foreach ($data as $key => $value) {
1176
        if ($value=='none') continue;
1177
        if ($value > $max) $max = $value;
1178
        if ($value < $min) $min = $value;
1179
      }
1180
 
1181
      if ($max == 0) {
1182
        $factor = 1;
1183
      } else {
1184
        if ($max < 0) $factor = - pow(10, (floor(log10(abs($max))) + $resolution) );
1185
        else $factor = pow(10, (floor(log10(abs($max))) - $resolution) );
1186
      }
1187
      if ($factor > 0.1) { // To avoid some wierd rounding errors (Moodle)
1441 ariadna 1188
        $factor = (int) round($factor * 1000.0) / 1000.0; // To avoid some wierd rounding errors (Moodle)
1 efrain 1189
      } // To avoid some wierd rounding errors (Moodle)
1190
 
1191
      $max = $factor * @ceil($max / $factor);
1192
      $min = $factor * @floor($min / $factor);
1193
 
1194
      //print "max=$max, min=$min<br />";
1195
 
1196
      return array('min' => $min, 'max' => $max);
1197
    }
1198
 
1199
    public function __construct() {
1200
      if (func_num_args() == 2) {
1201
        $this->parameter['width']  = func_get_arg(0);
1202
        $this->parameter['height'] = func_get_arg(1);
1203
      }
1204
      //$this->boundaryBox  = array(
1205
      $this->calculated['boundary_box'] = array(
1206
        'left'      =>  0,
1207
        'top'       =>  0,
1208
        'right'     =>  $this->parameter['width'] - 1,
1209
        'bottom'    =>  $this->parameter['height'] - 1);
1210
 
1211
      $this->init_colours();
1212
 
1213
      //ImageColorTransparent($this->image, $this->colour['white']); // colour for transparency
1214
    }
1215
 
1216
    /**
1217
     * Old syntax of class constructor. Deprecated in PHP7.
1218
     *
1219
     * @deprecated since Moodle 3.1
1220
     */
1221
    public function graph() {
1222
        debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
1223
        self::__construct();
1224
    }
1225
 
1226
    /**
1227
     * Prepare label's text for GD output.
1228
     *
1229
     * @param string    $label string to be prepared.
1230
     * @return string   Reversed input string, if we are in RTL mode and has no numbers.
1231
     *                  Otherwise, returns the string as is.
1232
     */
1233
    private function prepare_label_text($label) {
1234
        if (right_to_left() and !preg_match('/[0-9]/i', $label)) {
1235
            return core_text::strrev($label);
1236
        } else {
1237
            return $label;
1238
        }
1239
    }
1240
 
1241
    function print_TTF($message) {
1242
      $points    = $message['points'];
1243
      $angle     = $message['angle'];
1244
      // We have to manually reverse the label, since php GD cannot handle RTL characters properly in UTF8 strings.
1245
      $text      = $this->prepare_label_text($message['text']);
1246
      $colour    = $this->colour[$message['colour']];
1247
      $font      = $this->parameter['path_to_fonts'].$message['font'];
1248
 
1249
      $x         = $message['boundary_box']['x'];
1250
      $y         = $message['boundary_box']['y'];
1251
      $offsetX   = $message['boundary_box']['offsetX'];
1252
      $offsetY   = $message['boundary_box']['offsetY'];
1253
      $height    = $message['boundary_box']['height'];
1254
      $width     = $message['boundary_box']['width'];
1255
      $reference = $message['boundary_box']['reference'];
1256
 
1257
      switch ($reference) {
1258
        case 'top-left':
1259
        case 'left-top':
1260
          $y += $height - $offsetY;
1261
          //$y += $offsetY;
1262
          $x += $offsetX;
1263
          break;
1264
        case 'left-center':
1265
          $y += ($height / 2) - $offsetY;
1266
          $x += $offsetX;
1267
          break;
1268
        case 'left-bottom':
1269
          $y -= $offsetY;
1270
          $x += $offsetX;
1271
         break;
1272
        case 'top-center':
1273
          $y += $height - $offsetY;
1274
          $x -= ($width / 2) - $offsetX;
1275
         break;
1276
        case 'top-right':
1277
        case 'right-top':
1278
          $y += $height - $offsetY;
1279
          $x -= $width  - $offsetX;
1280
          break;
1281
        case 'right-center':
1282
          $y += ($height / 2) - $offsetY;
1283
          $x -= $width  - $offsetX;
1284
          break;
1285
        case 'right-bottom':
1286
          $y -= $offsetY;
1287
          $x -= $width  - $offsetX;
1288
          break;
1289
        case 'bottom-center':
1290
          $y -= $offsetY;
1291
          $x -= ($width / 2) - $offsetX;
1292
         break;
1293
        default:
1294
          $y = 0;
1295
          $x = 0;
1296
          break;
1297
      }
1298
      // start of Moodle addition
1299
      $text = core_text::utf8_to_entities($text, true, true); //does not work with hex entities!
1300
      // end of Moodle addition
1301
      [$x, $y] = [(int) round($x), (int) round($y)];
1302
      ImageTTFText($this->image, $points, $angle, $x, $y, $colour, $font, $text);
1303
    }
1304
 
1305
    // move boundaryBox to coordinates specified
1306
    function update_boundaryBox(&$boundaryBox, $coords) {
1307
      $width      = $boundaryBox['width'];
1308
      $height     = $boundaryBox['height'];
1309
      $x          = $coords['x'];
1310
      $y          = $coords['y'];
1311
      $reference  = $coords['reference'];
1312
      switch ($reference) {
1313
        case 'top-left':
1314
        case 'left-top':
1315
          $top    = $y;
1316
          $bottom = $y + $height;
1317
          $left   = $x;
1318
          $right  = $x + $width;
1319
          break;
1320
        case 'left-center':
1321
          $top    = $y - ($height / 2);
1322
          $bottom = $y + ($height / 2);
1323
          $left   = $x;
1324
          $right  = $x + $width;
1325
          break;
1326
        case 'left-bottom':
1327
          $top    = $y - $height;
1328
          $bottom = $y;
1329
          $left   = $x;
1330
          $right  = $x + $width;
1331
          break;
1332
        case 'top-center':
1333
          $top    = $y;
1334
          $bottom = $y + $height;
1335
          $left   = $x - ($width / 2);
1336
          $right  = $x + ($width / 2);
1337
          break;
1338
        case 'right-top':
1339
        case 'top-right':
1340
          $top    = $y;
1341
          $bottom = $y + $height;
1342
          $left   = $x - $width;
1343
          $right  = $x;
1344
          break;
1345
        case 'right-center':
1346
          $top    = $y - ($height / 2);
1347
          $bottom = $y + ($height / 2);
1348
          $left   = $x - $width;
1349
          $right  = $x;
1350
          break;
1351
        case 'bottom=right':
1352
        case 'right-bottom':
1353
          $top    = $y - $height;
1354
          $bottom = $y;
1355
          $left   = $x - $width;
1356
          $right  = $x;
1357
          break;
1358
        default:
1359
          $top    = 0;
1360
          $bottom = $height;
1361
          $left   = 0;
1362
          $right  = $width;
1363
          break;
1364
      }
1365
 
1366
      $boundaryBox = array_merge($boundaryBox, array('top'       => $top,
1367
                                                     'bottom'    => $bottom,
1368
                                                     'left'      => $left,
1369
                                                     'right'     => $right,
1370
                                                     'x'         => $x,
1371
                                                     'y'         => $y,
1372
                                                     'reference' => $reference));
1373
    }
1374
 
1375
    function get_null_size() {
1376
      return array('width'      => 0,
1377
                   'height'     => 0,
1378
                   'offsetX'    => 0,
1379
                   'offsetY'    => 0,
1380
                   //'fontHeight' => 0
1381
                   );
1382
    }
1383
 
1384
    function get_boundaryBox($message) {
1385
      $points  = $message['points'];
1386
      $angle   = $message['angle'];
1387
      $font    = $this->parameter['path_to_fonts'].$message['font'];
1388
      $text    = $message['text'];
1389
 
1390
      //print ('get_boundaryBox');
1391
      //expandPre($message);
1392
 
1393
      // get font size
1394
      $bounds = ImageTTFBBox($points, $angle, $font, "W");
1395
      if ($angle < 0) {
1396
        $fontHeight = abs($bounds[7]-$bounds[1]);
1397
      } else if ($angle > 0) {
1398
        $fontHeight = abs($bounds[1]-$bounds[7]);
1399
      } else {
1400
        $fontHeight = abs($bounds[7]-$bounds[1]);
1401
      }
1402
 
1403
      // get boundary box and offsets for printing at an angle
1404
      // start of Moodle addition
1405
      $text = core_text::utf8_to_entities($text, true, true); //gd does not work with hex entities!
1406
      // end of Moodle addition
1407
      $bounds = ImageTTFBBox($points, $angle, $font, $text);
1408
 
1409
      if ($angle < 0) {
1410
        $width = abs($bounds[4]-$bounds[0]);
1411
        $height = abs($bounds[3]-$bounds[7]);
1412
        $offsetY = abs($bounds[3]-$bounds[1]);
1413
        $offsetX = 0;
1414
 
1415
      } else if ($angle > 0) {
1416
        $width = abs($bounds[2]-$bounds[6]);
1417
        $height = abs($bounds[1]-$bounds[5]);
1418
        $offsetY = 0;
1419
        $offsetX = abs($bounds[0]-$bounds[6]);
1420
 
1421
      } else {
1422
        $width = abs($bounds[4]-$bounds[6]);
1423
        $height = abs($bounds[7]-$bounds[1]);
1424
        $offsetY = $bounds[1];
1425
        $offsetX = 0;
1426
      }
1427
 
1428
      //return values
1429
      return array('width'      => $width,
1430
                   'height'     => $height,
1431
                   'offsetX'    => $offsetX,
1432
                   'offsetY'    => $offsetY,
1433
                   //'fontHeight' => $fontHeight
1434
                   );
1435
    }
1436
 
1437
    function draw_rectangle($border, $colour, $type) {
1438
      $colour = $this->colour[$colour];
1439
      switch ($type) {
1440
        case 'fill':    // fill the rectangle
1441
          ImageFilledRectangle($this->image, $border['left'], $border['top'], $border['right'], $border['bottom'], $colour);
1442
          break;
1443
        case 'box':     // all sides
1444
          ImageRectangle($this->image, $border['left'], $border['top'], $border['right'], $border['bottom'], $colour);
1445
          break;
1446
        case 'axis':    // bottom x axis and left y axis
1447
          ImageLine($this->image, $border['left'], $border['top'], $border['left'], $border['bottom'], $colour);
1448
          ImageLine($this->image, $border['left'], $border['bottom'], $border['right'], $border['bottom'], $colour);
1449
          break;
1450
        case 'y':       // left y axis only
1451
        case 'y-left':
1452
          ImageLine($this->image, $border['left'], $border['top'], $border['left'], $border['bottom'], $colour);
1453
          break;
1454
        case 'y-right': // right y axis only
1455
          ImageLine($this->image, $border['right'], $border['top'], $border['right'], $border['bottom'], $colour);
1456
          break;
1457
        case 'x':       // bottom x axis only
1458
          ImageLine($this->image, $border['left'], $border['bottom'], $border['right'], $border['bottom'], $colour);
1459
          break;
1460
        case 'u':       // u shaped. bottom x axis and both left and right y axis.
1461
          ImageLine($this->image, $border['left'], $border['top'], $border['left'], $border['bottom'], $colour);
1462
          ImageLine($this->image, $border['right'], $border['top'], $border['right'], $border['bottom'], $colour);
1463
          ImageLine($this->image, $border['left'], $border['bottom'], $border['right'], $border['bottom'], $colour);
1464
          break;
1465
 
1466
      }
1467
    }
1468
 
1469
    function init_colours() {
1470
      $this->image              = ImageCreate($this->parameter['width'], $this->parameter['height']);
1471
      // standard colours
1472
      $this->colour['white']    = ImageColorAllocate ($this->image, 0xFF, 0xFF, 0xFF); // first colour is background colour.
1473
      $this->colour['black']    = ImageColorAllocate ($this->image, 0x00, 0x00, 0x00);
1474
      $this->colour['maroon']   = ImageColorAllocate ($this->image, 0x80, 0x00, 0x00);
1475
      $this->colour['green']    = ImageColorAllocate ($this->image, 0x00, 0x80, 0x00);
1476
      $this->colour['ltgreen']  = ImageColorAllocate ($this->image, 0x52, 0xF1, 0x7F);
1477
      $this->colour['ltltgreen']= ImageColorAllocate ($this->image, 0x99, 0xFF, 0x99);
1478
      $this->colour['olive']    = ImageColorAllocate ($this->image, 0x80, 0x80, 0x00);
1479
      $this->colour['navy']     = ImageColorAllocate ($this->image, 0x00, 0x00, 0x80);
1480
      $this->colour['purple']   = ImageColorAllocate ($this->image, 0x80, 0x00, 0x80);
1481
      $this->colour['gray']     = ImageColorAllocate ($this->image, 0x80, 0x80, 0x80);
1482
      $this->colour['red']      = ImageColorAllocate ($this->image, 0xFF, 0x00, 0x00);
1483
      $this->colour['ltred']    = ImageColorAllocate ($this->image, 0xFF, 0x99, 0x99);
1484
      $this->colour['ltltred']  = ImageColorAllocate ($this->image, 0xFF, 0xCC, 0xCC);
1485
      $this->colour['orange']   = ImageColorAllocate ($this->image, 0xFF, 0x66, 0x00);
1486
      $this->colour['ltorange']   = ImageColorAllocate ($this->image, 0xFF, 0x99, 0x66);
1487
      $this->colour['ltltorange'] = ImageColorAllocate ($this->image, 0xFF, 0xcc, 0x99);
1488
      $this->colour['lime']     = ImageColorAllocate ($this->image, 0x00, 0xFF, 0x00);
1489
      $this->colour['yellow']   = ImageColorAllocate ($this->image, 0xFF, 0xFF, 0x00);
1490
      $this->colour['blue']     = ImageColorAllocate ($this->image, 0x00, 0x00, 0xFF);
1491
      $this->colour['ltblue']   = ImageColorAllocate ($this->image, 0x00, 0xCC, 0xFF);
1492
      $this->colour['ltltblue'] = ImageColorAllocate ($this->image, 0x99, 0xFF, 0xFF);
1493
      $this->colour['fuchsia']  = ImageColorAllocate ($this->image, 0xFF, 0x00, 0xFF);
1494
      $this->colour['aqua']     = ImageColorAllocate ($this->image, 0x00, 0xFF, 0xFF);
1495
      //$this->colour['white']    = ImageColorAllocate ($this->image, 0xFF, 0xFF, 0xFF);
1496
      // shades of gray
1497
      $this->colour['grayF0']   = ImageColorAllocate ($this->image, 0xF0, 0xF0, 0xF0);
1498
      $this->colour['grayEE']   = ImageColorAllocate ($this->image, 0xEE, 0xEE, 0xEE);
1499
      $this->colour['grayDD']   = ImageColorAllocate ($this->image, 0xDD, 0xDD, 0xDD);
1500
      $this->colour['grayCC']   = ImageColorAllocate ($this->image, 0xCC, 0xCC, 0xCC);
1501
      $this->colour['gray33']   = ImageColorAllocate ($this->image, 0x33, 0x33, 0x33);
1502
      $this->colour['gray66']   = ImageColorAllocate ($this->image, 0x66, 0x66, 0x66);
1503
      $this->colour['gray99']   = ImageColorAllocate ($this->image, 0x99, 0x99, 0x99);
1504
 
1505
      $this->colour['none']   = 'none';
1506
      return true;
1507
    }
1508
 
1509
    function output() {
1510
      if ($this->debug) { // for debugging purposes.
1511
        //expandPre($this->graph);
1512
        //expandPre($this->y_data);
1513
        //expandPre($this->x_data);
1514
        //expandPre($this->parameter);
1515
      } else {
1516
 
1517
        $expiresSeconds = $this->parameter['seconds_to_live'];
1518
        $expiresHours = $this->parameter['hours_to_live'];
1519
 
1520
        if ($expiresHours || $expiresSeconds) {
1521
          $now = mktime (date("H"),date("i"),date("s"),date("m"),date("d"),date("Y"));
1522
          $expires = mktime (date("H")+$expiresHours,date("i"),date("s")+$expiresSeconds,date("m"),date("d"),date("Y"));
1523
          $expiresGMT = gmdate('D, d M Y H:i:s', $expires).' GMT';
1524
          $lastModifiedGMT  = gmdate('D, d M Y H:i:s', $now).' GMT';
1525
 
1526
          Header('Last-modified: '.$lastModifiedGMT);
1527
          Header('Expires: '.$expiresGMT);
1528
        }
1529
 
1530
        if ($this->parameter['file_name'] == 'none') {
1531
          switch ($this->parameter['output_format']) {
1532
            case 'GIF':
1533
              Header("Content-type: image/gif");  // GIF??. switch to PNG guys!!
1534
              ImageGIF($this->image);
1535
              break;
1536
            case 'JPEG':
1537
              Header("Content-type: image/jpeg"); // JPEG for line art??. included for completeness.
1538
              ImageJPEG($this->image);
1539
              break;
1540
           default:
1541
              Header("Content-type: image/png");  // preferred output format
1542
              ImagePNG($this->image);
1543
              break;
1544
          }
1545
        } else {
1546
           switch ($this->parameter['output_format']) {
1547
            case 'GIF':
1548
              ImageGIF($this->image, $this->parameter['file_name'].'.gif');
1549
              break;
1550
            case 'JPEG':
1551
              ImageJPEG($this->image, $this->parameter['file_name'].'.jpg');
1552
              break;
1553
           default:
1554
              ImagePNG($this->image, $this->parameter['file_name'].'.png');
1555
              break;
1556
          }
1557
        }
1558
 
1559
        ImageDestroy($this->image);
1560
      }
1561
    } // function output
1562
 
1563
    function init_variable(&$variable, $value, $default) {
1564
      if (!empty($value)) $variable = $value;
1565
      else if (isset($default)) $variable = $default;
1566
      else unset($variable);
1567
    }
1568
 
1569
    // plot a point. options include square, circle, diamond, triangle, and dot. offset is used for drawing shadows.
1570
    // for diamonds and triangles the size should be an even number to get nice look. if odd the points are crooked.
1571
    function plot($x, $y, $type, $size, $colour, $offset) {
1572
      //print("drawing point of type: $type, at offset: $offset");
1573
      $u = $x + $offset;
1574
      $v = $this->calculated['inner_border']['bottom'] - $y + $offset;
1575
      $half = $size / 2;
1576
      [$u, $v, $half] = [(int) round($u), (int) round($v), (int) round($half)];
1577
      switch ($type) {
1578
        case 'square':
1579
          ImageFilledRectangle($this->image, $u-$half, $v-$half, $u+$half, $v+$half, $this->colour[$colour]);
1580
          break;
1581
        case 'square-open':
1582
          ImageRectangle($this->image, $u-$half, $v-$half, $u+$half, $v+$half, $this->colour[$colour]);
1583
          break;
1584
        case 'circle':
1585
          ImageArc($this->image, $u, $v, $size, $size, 0, 360, $this->colour[$colour]);
1586
          ImageFillToBorder($this->image, $u, $v, $this->colour[$colour], $this->colour[$colour]);
1587
          break;
1588
        case 'circle-open':
1589
          ImageArc($this->image, $u, $v, $size, $size, 0, 360, $this->colour[$colour]);
1590
          break;
1591
        case 'diamond':
1441 ariadna 1592
          ImageFilledPolygon($this->image, [$u, $v - $half, $u + $half, $v, $u, $v + $half, $u - $half, $v], $this->colour[$colour]);
1 efrain 1593
          break;
1594
        case 'diamond-open':
1441 ariadna 1595
          ImagePolygon($this->image, [$u, $v - $half, $u + $half, $v, $u, $v + $half, $u - $half, $v], $this->colour[$colour]);
1 efrain 1596
          break;
1597
        case 'triangle':
1441 ariadna 1598
          ImageFilledPolygon($this->image, [$u, $v - $half, $u + $half, $v + $half, $u - $half, $v + $half], $this->colour[$colour]);
1 efrain 1599
          break;
1600
        case 'triangle-open':
1441 ariadna 1601
          ImagePolygon($this->image, [$u, $v - $half, $u + $half, $v + $half, $u - $half, $v + $half], $this->colour[$colour]);
1 efrain 1602
          break;
1603
        case 'dot':
1604
          ImageSetPixel($this->image, $u, $v, $this->colour[$colour]);
1605
          break;
1606
      }
1607
    }
1608
 
1609
    function bar($x, $y, $type, $size, $colour, $offset, $index, $yoffset) {
1610
      $index_offset = $this->calculated['bar_offset_index'][$index];
1611
      if ( $yoffset ) {
1612
        $bar_offsetx = 0;
1613
      } else {
1614
        $bar_offsetx = $this->calculated['bar_offset_x'][$index_offset];
1615
      }
1616
      //$this->dbug("drawing bar at offset = $offset : index = $index: bar_offsetx = $bar_offsetx");
1617
 
1618
      $span = ($this->calculated['bar_width'] * $size) / 2;
1619
      $x_left  = $x + $bar_offsetx - $span;
1620
      $x_right = $x + $bar_offsetx + $span;
1621
 
1622
      if ($this->parameter['zero_axis'] != 'none') {
1623
        $zero = $this->calculated['zero_axis'];
1624
        if ($this->parameter['shadow_below_axis'] ) $zero  += $offset;
1625
        $u_left  = (int) round($x_left + $offset);
1626
        $u_right = (int) round($x_right + $offset - 1);
1627
        $v       = $this->calculated['boundary_box']['bottom'] - $y + $offset;
1628
 
1629
        if ($v > $zero) {
1630
          $top = $zero +1;
1631
          $bottom = $v;
1632
        } else {
1633
          $top = $v;
1634
          $bottom = $zero - 1;
1635
        }
1636
 
1637
        [$top, $bottom]  = [(int) round($top), (int) round($bottom)];
1638
 
1639
        switch ($type) {
1640
          case 'open':
1641
            if ($v > $zero)
1642
              ImageRectangle($this->image, $u_left, $bottom, $u_right, $bottom, $this->colour[$colour]);
1643
            else
1644
              ImageRectangle($this->image, $u_left, $top, $u_right, $top, $this->colour[$colour]);
1645
            ImageRectangle($this->image, $u_left, $top, $u_left, $bottom, $this->colour[$colour]);
1646
            ImageRectangle($this->image, $u_right, $top, $u_right, $bottom, $this->colour[$colour]);
1647
            break;
1648
          case 'fill':
1649
            ImageFilledRectangle($this->image, $u_left, $top, $u_right, $bottom, $this->colour[$colour]);
1650
            break;
1651
        }
1652
 
1653
      } else {
1654
 
1655
        $bottom = $this->calculated['boundary_box']['bottom'];
1656
        if ($this->parameter['shadow_below_axis'] ) $bottom  += $offset;
1657
        if ($this->parameter['inner_border'] != 'none') $bottom -= 1; // 1 pixel above bottom if border is to be drawn.
1658
        $u_left  = (int) round($x_left + $offset);
1659
        $u_right = (int) round($x_right + $offset - 1);
1660
        $v       = $this->calculated['boundary_box']['bottom'] - $y + $offset;
1661
 
1662
        // Moodle addition, plus the function parameter yoffset
1663
        if ($yoffset) {                                           // Moodle
1441 ariadna 1664
            $yoffset = $yoffset - (int) round(($bottom - $v) / 2.0);    // Moodle
1 efrain 1665
            $bottom -= $yoffset;                                  // Moodle
1666
            $v      -= $yoffset;                                  // Moodle
1667
        }                                                         // Moodle
1668
 
1669
        [$v, $bottom] = [(int) round($v), (int) round($bottom)];
1670
 
1671
        switch ($type) {
1672
          case 'open':
1673
            ImageRectangle($this->image, $u_left, $v, $u_right, $bottom, $this->colour[$colour]);
1674
            break;
1675
          case 'fill':
1676
            ImageFilledRectangle($this->image, $u_left, $v, $u_right, $bottom, $this->colour[$colour]);
1677
            break;
1678
        }
1679
      }
1680
    }
1681
 
1682
    function area($x_start, $y_start, $x_end, $y_end, $type, $colour, $offset) {
1683
      //dbug("drawing area type: $type, at offset: $offset");
1684
      if ($this->parameter['zero_axis'] != 'none') {
1685
        $bottom = $this->calculated['boundary_box']['bottom'];
1686
        $zero   = $this->calculated['zero_axis'];
1687
        if ($this->parameter['shadow_below_axis'] ) $zero  += $offset;
1688
        $u_start = $x_start + $offset;
1689
        $u_end   = $x_end + $offset;
1690
        $v_start = $bottom - $y_start + $offset;
1691
        $v_end   = $bottom - $y_end + $offset;
1692
        switch ($type) {
1693
          case 'fill':
1694
            // draw it this way 'cos the FilledPolygon routine seems a bit buggy.
1441 ariadna 1695
            ImageFilledPolygon($this->image, [$u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero], $this->colour[$colour]);
1696
            ImagePolygon($this->image, [$u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero], $this->colour[$colour]);
1 efrain 1697
            break;
1698
          case 'open':
1699
            ImageLine($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]);
1700
            ImageLine($this->image, $u_start, $v_start, $u_start, $zero, $this->colour[$colour]);
1701
            ImageLine($this->image, $u_end, $v_end, $u_end, $zero, $this->colour[$colour]);
1702
           break;
1703
        }
1704
      } else {
1705
        $bottom = $this->calculated['boundary_box']['bottom'];
1706
        $u_start = $x_start + $offset;
1707
        $u_end   = $x_end + $offset;
1708
        $v_start = $bottom - $y_start + $offset;
1709
        $v_end   = $bottom - $y_end + $offset;
1710
 
1711
        if ($this->parameter['shadow_below_axis'] ) $bottom  += $offset;
1712
        if ($this->parameter['inner_border'] != 'none') $bottom -= 1; // 1 pixel above bottom if border is to be drawn.
1713
        switch ($type) {
1714
          case 'fill':
1441 ariadna 1715
            ImageFilledPolygon($this->image, [$u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom], $this->colour[$colour]);
1 efrain 1716
           break;
1441 ariadna 1717
          case 'open':
1718
            ImagePolygon($this->image, [$u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom], $this->colour[$colour]);
1 efrain 1719
            break;
1720
        }
1721
      }
1722
    }
1723
 
1724
    function line($x_start, $y_start, $x_end, $y_end, $type, $brush_type, $brush_size, $colour, $offset) {
1725
      //dbug("drawing line of type: $type, at offset: $offset");
1726
      $u_start = (int) round($x_start + $offset);
1727
      $v_start = (int) round($this->calculated['boundary_box']['bottom'] - $y_start + $offset);
1728
      $u_end   = (int) round($x_end + $offset);
1729
      $v_end   = (int) round($this->calculated['boundary_box']['bottom'] - $y_end + $offset);
1730
 
1731
      switch ($type) {
1732
        case 'brush':
1733
          $this->draw_brush_line($u_start, $v_start, $u_end, $v_end, $brush_size, $brush_type, $colour);
1734
         break;
1735
        case 'line' :
1736
          ImageLine($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]);
1737
          break;
1738
        case 'dash':
1739
          $this->image_dashed_line($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]); // Moodle
1740
          break;
1741
      }
1742
    }
1743
 
1744
    // function to draw line. would prefer to use gdBrush but this is not supported yet.
1745
    function draw_brush_line($x0, $y0, $x1, $y1, $size, $type, $colour) {
1746
      //$this->dbug("line: $x0, $y0, $x1, $y1");
1747
      $dy = $y1 - $y0;
1748
      $dx = $x1 - $x0;
1749
      $t = 0;
1750
      $watchdog = 1024; // precaution to prevent infinite loops.
1751
 
1752
      $this->draw_brush($x0, $y0, $size, $type, $colour);
1753
      if (abs($dx) > abs($dy)) { // slope < 1
1754
        //$this->dbug("slope < 1");
1755
        $m = $dy / $dx; // compute slope
1756
        $t += $y0;
1757
        $dx = ($dx < 0) ? -1 : 1;
1758
        $m *= $dx;
1441 ariadna 1759
        while ((int) round($x0) != (int) round($x1)) {
1 efrain 1760
          if (!$watchdog--) break;
1761
          $x0 += $dx; // step to next x value
1762
          $t += $m;   // add slope to y value
1441 ariadna 1763
          $y = (int) round($t);
1 efrain 1764
          //$this->dbug("x0=$x0, x1=$x1, y=$y watchdog=$watchdog");
1765
          $this->draw_brush($x0, $y, $size, $type, $colour);
1766
 
1767
        }
1768
      } else { // slope >= 1
1769
        //$this->dbug("slope >= 1");
1770
        $m = $dx / $dy; // compute slope
1771
        $t += $x0;
1772
        $dy = ($dy < 0) ? -1 : 1;
1773
        $m *= $dy;
1441 ariadna 1774
        while ((int) round($y0) != (int) round($y1)) {
1 efrain 1775
          if (!$watchdog--) break;
1776
          $y0 += $dy; // step to next y value
1777
          $t += $m;   // add slope to x value
1441 ariadna 1778
          $x = (int) round($t);
1 efrain 1779
          //$this->dbug("x=$x, y0=$y0, y1=$y1 watchdog=$watchdog");
1780
          $this->draw_brush($x, $y0, $size, $type, $colour);
1781
 
1782
        }
1783
      }
1784
    }
1785
 
1786
    function draw_brush($x, $y, $size, $type, $colour) {
1441 ariadna 1787
      $x = (int) round($x);
1788
      $y = (int) round($y);
1789
      $half = (int) round($size / 2);
1 efrain 1790
      switch ($type) {
1791
        case 'circle':
1792
          ImageArc($this->image, $x, $y, $size, $size, 0, 360, $this->colour[$colour]);
1793
          ImageFillToBorder($this->image, $x, $y, $this->colour[$colour], $this->colour[$colour]);
1794
          break;
1795
        case 'square':
1796
          ImageFilledRectangle($this->image, $x-$half, $y-$half, $x+$half, $y+$half, $this->colour[$colour]);
1797
          break;
1798
        case 'vertical':
1799
          ImageFilledRectangle($this->image, $x, $y-$half, $x+1, $y+$half, $this->colour[$colour]);
1800
          break;
1801
        case 'horizontal':
1802
          ImageFilledRectangle($this->image, $x-$half, $y, $x+$half, $y+1, $this->colour[$colour]);
1803
          break;
1804
        case 'slash':
1441 ariadna 1805
          ImageFilledPolygon(
1806
            $this->image,
1807
            [
1 efrain 1808
              $x + $half, $y - $half,
1809
              $x + $half + 1, $y - $half,
1810
              $x - $half + 1, $y + $half,
1441 ariadna 1811
              $x - $half, $y + $half,
1812
            ],
1813
            $this->colour[$colour]
1814
          );
1 efrain 1815
          break;
1816
        case 'backslash':
1441 ariadna 1817
          ImageFilledPolygon(
1818
            $this->image,
1819
            [
1 efrain 1820
              $x - $half, $y - $half,
1821
              $x - $half + 1, $y - $half,
1822
              $x + $half + 1, $y + $half,
1441 ariadna 1823
              $x + $half, $y + $half,
1824
            ],
1825
            $this->colour[$colour]
1826
          );
1 efrain 1827
          break;
1828
        default:
1829
          @eval($type); // user can create own brush script.
1830
      }
1831
    }
1832
 
1833
    /**
1834
     * Moodle.
1835
     *
1836
     * A replacement for deprecated ImageDashedLine function.
1837
     *
1838
     * @param resource|GdImage $image
1839
     * @param int $x1 — x-coordinate for first point.
1840
     * @param int $y1 — y-coordinate for first point.
1841
     * @param int $x2 — x-coordinate for second point.
1842
     * @param int $y2 — y-coordinate for second point.
1843
     * @param int $color
1844
     * @return void
1845
     */
1846
    private function image_dashed_line($image, $x1, $y1, $x2, $y2, $colour): void {
1847
      // Create a dashed style.
1848
      $style = array(
1849
        $colour,
1850
        $colour,
1851
        $colour,
1852
        $colour,
1853
        IMG_COLOR_TRANSPARENT,
1854
        IMG_COLOR_TRANSPARENT,
1855
        IMG_COLOR_TRANSPARENT,
1856
        IMG_COLOR_TRANSPARENT
1857
      );
1858
      imagesetstyle($image, $style);
1859
 
1860
      // Apply the dashed style.
1861
      imageline($image, $x1, $y1, $x2, $y2, IMG_COLOR_STYLED);
1862
    }
1863
 
1864
} // class graph