weekly on-sync release 5.0dev
[moodle.git] / lib / graphlib.php
blob0eebb05728a421eb278225f7617d625527ae62ab
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
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.
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.
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
21 * Copy of GNU Lesser General Public License at: http://www.gnu.org/copyleft/lesser.txt
22 * Contact author at: hermanV@mindless.com
24 * @package core
25 * @subpackage lib
28 declare(strict_types=1);
30 defined('MOODLE_INTERNAL') || die();
32 /* This file contains modifications by Martin Dougiamas
33 * as part of Moodle (http://moodle.com). Modified lines
34 * are marked with "Moodle".
37 /**
38 * @package moodlecore
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'.
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.
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
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.
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
70 'x_label_angle' => 90, // rotation of y axis label
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.
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.
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.
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.
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'.
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.
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.
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.
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'.
150 'decimal_point' => '.', // symbol for decimal separation '.' or ',' *european support.
151 'thousand_sep' => ',', // symbol for thousand separation ',' or ''
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
157 /** @var array y_order data. */
158 public $y_order = [];
160 /** @var array y_format data. */
161 public $y_format = [];
163 /** @var array x_data data. */
164 public $x_data = [];
166 /** @var array colour. */
167 public $colour = [];
169 /** @var array y_data data. */
170 public $y_data = [];
172 // init all text - title, labels, and axis text.
173 function init() {
175 /// Moodle mods: overrides the font path and encodings
177 global $CFG;
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/
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/";
199 $this->parameter['path_to_fonts'] = $fontpath;
201 /// End Moodle mods
205 $this->calculated['outer_border'] = $this->calculated['boundary_box'];
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'];
213 $this->init_x_axis();
214 $this->init_y_axis();
215 $this->init_legend();
216 $this->init_labels();
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'];
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'];
229 $this->calculated['boundary_box']['left'] += $this->calculated['left_inner_padding'];
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'];
236 $this->calculated['boundary_box']['right'] -= $this->calculated['right_inner_padding'];
238 // boundaryBox now has coords for plotting area.
239 $this->calculated['inner_border'] = $this->calculated['boundary_box'];
241 $this->init_data();
242 $this->init_x_ticks();
243 $this->init_y_ticks();
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
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
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();
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
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
273 function draw_stack() {
274 $this->init();
275 $this->draw_text();
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();
284 $this->y_order = $yOrder; // revert y_order data.
286 $this->output();
289 function draw() {
290 $this->init();
291 $this->draw_text();
292 $this->draw_data();
293 $this->output();
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');
309 $lastX = 0;
310 $lastY = 'none';
311 $fromX = 0;
312 $fromY = 'none';
314 //print "set $set<br />";
315 //expand_pre($this->calculated['y_plot']);
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];
322 //print "$thisX, $thisY <br />";
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
334 if (($area!='none') && (((string)$lastY != 'none') && ((string)$thisY != 'none')))
335 $this->area($lastX, $lastY, $thisX, $thisY, $area, $colour, $offset);
337 if (($point!='none') && (string)$thisY != 'none') $this->plot($thisX, $thisY, $point, $pointSize, $colour, $offset);
339 if (($line!='none') && ((string)$thisY != 'none')) {
340 if ((string)$fromY != 'none')
341 $this->line($fromX, $fromY, $thisX, $thisY, $line, $brushType, $brushSize, $colour, $offset);
343 $fromY = $thisY; // start next line from here
344 $fromX = $thisX; // ...
345 } else {
346 $fromY = 'none';
347 $fromX = 'none';
350 $lastX = $thisX;
351 $lastY = $thisY;
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);
365 // then draw data
366 foreach ($this->y_order as $order => $set) {
367 $this->draw_set($order, $set, 0);
371 function draw_legend() {
372 $position = $this->parameter['legend'];
373 if ($position == 'none') return; // abort if no border
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;
393 break;
394 case 'top-right':
395 $top = $graphTop + $offset;
396 $bottom = $graphTop + $height + $offset;
397 $left = $graphRight - $width - $offset;
398 $right = $graphRight - $offset;
400 break;
401 case 'bottom-left':
402 $top = $graphBottom - $height - $offset;
403 $bottom = $graphBottom - $offset;
404 $left = $graphLeft + $offset;
405 $right = $graphLeft + $width + $offset;
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;
415 case 'outside-top' :
416 $top = $graphTop;
417 $bottom = $graphTop + $height;
418 $left = $outsideRight - $width - $offset;
419 $right = $outsideRight - $offset;
420 break;
422 case 'outside-bottom' :
423 $top = $graphBottom - $height;
424 $bottom = $graphBottom;
425 $left = $outsideRight - $width - $offset;
426 $right = $outsideRight - $offset;
427 break;
429 case 'outside-left' :
430 $top = $outsideBottom - $height - $offset;
431 $bottom = $outsideBottom - $offset;
432 $left = $graphLeft;
433 $right = $graphLeft + $width;
434 break;
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'];
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');
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']);
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;
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']];
472 // draw box
473 ImageFilledRectangle($this->image, $x, $y, $x + $box, $y + $box, $boxColour);
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;
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;
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);
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;
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);
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);
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);
536 function draw_zero_axis_left() {
537 $colour = $this->parameter['zero_axis'];
538 if ($colour == 'none') return;
539 // draw zero axis on left hand side
540 $this->calculated['zero_axis'] = (int) round(
541 $this->calculated['boundary_box']['top'] +
542 ($this->calculated['y_axis_left']['max'] * $this->calculated['y_axis_left']['factor'])
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]);
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
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'])
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]);
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']);
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'];
576 $axis_font = $this->parameter['axis_font'];
577 $axis_size = $this->parameter['axis_size'];
578 $axis_angle = $this->parameter['x_axis_angle'];
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';
585 //generic tag information. applies to all axis text.
586 $axisTag = array('points' => $axis_size, 'angle' => $axis_angle, 'font' => $axis_font, 'colour' => $axis_colour);
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;
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);
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);
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']);
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);
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'];
642 $textRight = $tickLeft - $this->calculated['left_inner_padding'];
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';
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;
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);
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);
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'];
689 $textLeft = $tickRight+ $this->calculated['left_inner_padding'];
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';
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':
700 ImageLine($this->image, (int) round($gridLeft), (int) round($tickY), (int) round($gridRight), (int) round($tickY), $gridColour);
701 break;
702 case 'dash':
703 $this->image_dashed_line($this->image, (int) round($gridLeft), (int) round($tickY), (int) round($gridRight), (int) round($tickY), $gridColour); // Moodle
704 break;
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')
711 ImageLine($this->image, (int) round($tickLeft), (int) round($tickY), (int) round($tickRight), (int) round($tickY), $tickColour);
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);
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'];
729 // calculate pixel steps between axis ticks.
730 $this->calculated['y_axis']['step'] = $height / ($this->parameter['y_axis_gridlines'] - 1);
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
736 // Hack by rodger to avoid division by zero, see bug 1231
737 if ($numTicks==0) $numTicks=1;
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;
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;
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;
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']++;
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];
772 if ((string)$this->y_data[$set][$index] != 'none') {
774 if (isset($this->y_format[$set]['y_axis']) && $this->y_format[$set]['y_axis'] == 'right') {
775 $this->calculated['y_plot'][$set][$index] =
776 (int) round(($this->y_data[$set][$index] - $this->calculated['y_axis_right']['min'])
777 * $this->calculated['y_axis_right']['factor']);
778 } else {
779 //print "$set $index<br />";
780 $this->calculated['y_plot'][$set][$index] =
781 (int) round(($this->y_data[$set][$index] - $this->calculated['y_axis_left']['min'])
782 * $this->calculated['y_axis_left']['factor']);
788 //print "factor ".$this->calculated['x_axis']['factor']."<br />";
789 //expand_pre($this->calculated['x_plot']);
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'];
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.
802 $this->calculated['bar_width'] = $barWidth;
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
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
821 if ($this->parameter['x_axis_gridlines'] == 'auto') $this->calculated['x_plot'][$set] = (int) round($tickX);
822 //print $this->calculated['x_plot'][$set].'<br />';
823 $tickX += $xStep;
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;
832 if (empty($this->calculated['x_axis']['min'])) {
833 $this->calculated['x_axis']['min'] = 0;
835 $factor = $this->calculated['x_axis']['factor'];
836 $min = $this->calculated['x_axis']['min'];
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'];
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']);
847 $this->calculated['x_plot'][$index] = $gridLeft + ($x - $min) * $factor;
849 //print $this->calculated['x_plot'][$set].'<br />';
852 //expand_pre($this->calculated['boundary_box']);
853 //print "factor ".$this->calculated['x_axis']['factor']."<br />";
854 //expand_pre($this->calculated['x_plot']);
857 function init_y_ticks() {
858 // get coords for y axis ticks
860 $yStep = $this->calculated['y_axis']['step'];
861 $gridBottom = $this->calculated['boundary_box']['bottom'];
862 $tickY = $gridBottom; // tick y coord
864 for ($i = 0; $i < $this->parameter['y_axis_gridlines']; $i++) {
865 $this->calculated['y_axis']['tick_y'][$i] = $tickY;
866 $tickY -= $yStep;
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;
885 $this->calculated['boundary_box']['top'] += $size['height'] + $this->parameter['outer_padding'];
886 //$this->calculated['boundary_box']['top'] += $size['height'];
888 } else $this->calculated['title']['boundary_box'] = $this->get_null_size();
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'];
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'];
902 } else $this->calculated['y_label_left']['boundary_box'] = $this->get_null_size();
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'];
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'];
916 } else $this->calculated['y_label_right']['boundary_box'] = $this->get_null_size();
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'];
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'];
930 } else $this->calculated['x_label']['boundary_box'] = $this->get_null_size();
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;
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;
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));
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;
961 if ($text && $text!='none') {
962 $numSets++;
963 $sumTextHeight += $size['height'];
966 if ($size['width'] > $this->calculated['legend']['boundary_box_max']['width'])
967 $this->calculated['legend']['boundary_box_max'] = $size;
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.
977 $this->calculated['legend']['boundary_box_all'] = array('width' => $width,
978 'height' => $height,
979 'offset' => $offset,
980 'reference' => $position);
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;
987 case 'outside-bottom' :
988 $this->calculated['boundary_box']['right'] -= $offset + $width; // move in right hand side
989 break;
991 case 'outside-left' :
992 $this->calculated['boundary_box']['bottom'] -= $offset + $height; // move in right hand side
993 break;
995 case 'outside-right' :
996 $this->calculated['boundary_box']['bottom'] -= $offset + $height; // move in right hand side
997 break;
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();
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;
1013 $this->calculated['y_axis_left']['has_data'] = FALSE;
1014 $this->calculated['y_axis_right']['has_data'] = FALSE;
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]);
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'];
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;
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;
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']);
1057 $this->calculated['y_axis_left']['data'][$i] = $startLeft;
1058 $this->calculated['y_axis_left']['text'][$i] = $value; // text is formatted raw data
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;
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'];
1073 $startLeft += $stepLeft;
1075 $this->calculated['boundary_box']['left'] += $this->calculated['y_axis_left']['boundary_box_max']['width']
1076 + $this->parameter['y_inner_padding'];
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;
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'];
1098 $start_right += $step_right;
1100 $this->calculated['boundary_box']['right'] -= $this->calculated['y_axis_right']['boundary_box_max']['width']
1101 + $this->parameter['y_inner_padding'];
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);
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'];
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;
1133 } else { // x axis is numeric so find max min values...
1134 $this->calculated['x_axis']['num_ticks'] = $this->parameter['x_axis_gridlines'];
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;
1145 $step = ($max - $min) / ($this->calculated['x_axis']['num_ticks'] - 1);
1146 $start = $min;
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
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;
1161 if ($size['height'] > $this->calculated['x_axis']['boundary_box_max']['height'])
1162 $this->calculated['x_axis']['boundary_box_max'] = $size;
1164 $start += $step;
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'];
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;
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) );
1187 if ($factor > 0.1) { // To avoid some wierd rounding errors (Moodle)
1188 $factor = (int) round($factor * 1000.0) / 1000.0; // To avoid some wierd rounding errors (Moodle)
1189 } // To avoid some wierd rounding errors (Moodle)
1191 $max = $factor * @ceil($max / $factor);
1192 $min = $factor * @floor($min / $factor);
1194 //print "max=$max, min=$min<br />";
1196 return array('min' => $min, 'max' => $max);
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);
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);
1211 $this->init_colours();
1213 //ImageColorTransparent($this->image, $this->colour['white']); // colour for transparency
1217 * Old syntax of class constructor. Deprecated in PHP7.
1219 * @deprecated since Moodle 3.1
1221 public function graph() {
1222 debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
1223 self::__construct();
1227 * Prepare label's text for GD output.
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.
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;
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'];
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'];
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;
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);
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;
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));
1375 function get_null_size() {
1376 return array('width' => 0,
1377 'height' => 0,
1378 'offsetX' => 0,
1379 'offsetY' => 0,
1380 //'fontHeight' => 0
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'];
1390 //print ('get_boundaryBox');
1391 //expandPre($message);
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]);
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);
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;
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]);
1421 } else {
1422 $width = abs($bounds[4]-$bounds[6]);
1423 $height = abs($bounds[7]-$bounds[1]);
1424 $offsetY = $bounds[1];
1425 $offsetX = 0;
1428 //return values
1429 return array('width' => $width,
1430 'height' => $height,
1431 'offsetX' => $offsetX,
1432 'offsetY' => $offsetY,
1433 //'fontHeight' => $fontHeight
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;
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);
1505 $this->colour['none'] = 'none';
1506 return true;
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 {
1517 $expiresSeconds = $this->parameter['seconds_to_live'];
1518 $expiresHours = $this->parameter['hours_to_live'];
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';
1526 Header('Last-modified: '.$lastModifiedGMT);
1527 Header('Expires: '.$expiresGMT);
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;
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;
1559 ImageDestroy($this->image);
1561 } // function output
1563 function init_variable(&$variable, $value, $default) {
1564 if (!empty($value)) $variable = $value;
1565 else if (isset($default)) $variable = $default;
1566 else unset($variable);
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':
1592 if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
1593 ImageFilledPolygon($this->image, array($u, $v - $half, $u + $half, $v, $u, $v + $half, $u - $half, $v), $this->colour[$colour]);
1594 } else {
1595 ImageFilledPolygon($this->image, array($u, $v - $half, $u + $half, $v, $u, $v + $half, $u - $half, $v), 4, $this->colour[$colour]);
1597 break;
1598 case 'diamond-open':
1599 if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
1600 ImagePolygon($this->image, array($u, $v - $half, $u + $half, $v, $u, $v + $half, $u - $half, $v), $this->colour[$colour]);
1601 } else {
1602 ImagePolygon($this->image, array($u, $v - $half, $u + $half, $v, $u, $v + $half, $u - $half, $v), 4, $this->colour[$colour]);
1604 break;
1605 case 'triangle':
1606 if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
1607 ImageFilledPolygon($this->image, array($u, $v - $half, $u + $half, $v + $half, $u - $half, $v + $half), $this->colour[$colour]);
1608 } else {
1609 ImageFilledPolygon($this->image, array($u, $v - $half, $u + $half, $v + $half, $u - $half, $v + $half), 3, $this->colour[$colour]);
1611 break;
1612 case 'triangle-open':
1613 if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
1614 ImagePolygon($this->image, array($u, $v - $half, $u + $half, $v + $half, $u - $half, $v + $half), $this->colour[$colour]);
1615 } else {
1616 ImagePolygon($this->image, array($u, $v - $half, $u + $half, $v + $half, $u - $half, $v + $half), 3, $this->colour[$colour]);
1618 break;
1619 case 'dot':
1620 ImageSetPixel($this->image, $u, $v, $this->colour[$colour]);
1621 break;
1625 function bar($x, $y, $type, $size, $colour, $offset, $index, $yoffset) {
1626 $index_offset = $this->calculated['bar_offset_index'][$index];
1627 if ( $yoffset ) {
1628 $bar_offsetx = 0;
1629 } else {
1630 $bar_offsetx = $this->calculated['bar_offset_x'][$index_offset];
1632 //$this->dbug("drawing bar at offset = $offset : index = $index: bar_offsetx = $bar_offsetx");
1634 $span = ($this->calculated['bar_width'] * $size) / 2;
1635 $x_left = $x + $bar_offsetx - $span;
1636 $x_right = $x + $bar_offsetx + $span;
1638 if ($this->parameter['zero_axis'] != 'none') {
1639 $zero = $this->calculated['zero_axis'];
1640 if ($this->parameter['shadow_below_axis'] ) $zero += $offset;
1641 $u_left = (int) round($x_left + $offset);
1642 $u_right = (int) round($x_right + $offset - 1);
1643 $v = $this->calculated['boundary_box']['bottom'] - $y + $offset;
1645 if ($v > $zero) {
1646 $top = $zero +1;
1647 $bottom = $v;
1648 } else {
1649 $top = $v;
1650 $bottom = $zero - 1;
1653 [$top, $bottom] = [(int) round($top), (int) round($bottom)];
1655 switch ($type) {
1656 case 'open':
1657 if ($v > $zero)
1658 ImageRectangle($this->image, $u_left, $bottom, $u_right, $bottom, $this->colour[$colour]);
1659 else
1660 ImageRectangle($this->image, $u_left, $top, $u_right, $top, $this->colour[$colour]);
1661 ImageRectangle($this->image, $u_left, $top, $u_left, $bottom, $this->colour[$colour]);
1662 ImageRectangle($this->image, $u_right, $top, $u_right, $bottom, $this->colour[$colour]);
1663 break;
1664 case 'fill':
1665 ImageFilledRectangle($this->image, $u_left, $top, $u_right, $bottom, $this->colour[$colour]);
1666 break;
1669 } else {
1671 $bottom = $this->calculated['boundary_box']['bottom'];
1672 if ($this->parameter['shadow_below_axis'] ) $bottom += $offset;
1673 if ($this->parameter['inner_border'] != 'none') $bottom -= 1; // 1 pixel above bottom if border is to be drawn.
1674 $u_left = (int) round($x_left + $offset);
1675 $u_right = (int) round($x_right + $offset - 1);
1676 $v = $this->calculated['boundary_box']['bottom'] - $y + $offset;
1678 // Moodle addition, plus the function parameter yoffset
1679 if ($yoffset) { // Moodle
1680 $yoffset = $yoffset - (int) round(($bottom - $v) / 2.0); // Moodle
1681 $bottom -= $yoffset; // Moodle
1682 $v -= $yoffset; // Moodle
1683 } // Moodle
1685 [$v, $bottom] = [(int) round($v), (int) round($bottom)];
1687 switch ($type) {
1688 case 'open':
1689 ImageRectangle($this->image, $u_left, $v, $u_right, $bottom, $this->colour[$colour]);
1690 break;
1691 case 'fill':
1692 ImageFilledRectangle($this->image, $u_left, $v, $u_right, $bottom, $this->colour[$colour]);
1693 break;
1698 function area($x_start, $y_start, $x_end, $y_end, $type, $colour, $offset) {
1699 //dbug("drawing area type: $type, at offset: $offset");
1700 if ($this->parameter['zero_axis'] != 'none') {
1701 $bottom = $this->calculated['boundary_box']['bottom'];
1702 $zero = $this->calculated['zero_axis'];
1703 if ($this->parameter['shadow_below_axis'] ) $zero += $offset;
1704 $u_start = $x_start + $offset;
1705 $u_end = $x_end + $offset;
1706 $v_start = $bottom - $y_start + $offset;
1707 $v_end = $bottom - $y_end + $offset;
1708 switch ($type) {
1709 case 'fill':
1710 // draw it this way 'cos the FilledPolygon routine seems a bit buggy.
1711 if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
1712 ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), $this->colour[$colour]);
1713 ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), $this->colour[$colour]);
1714 } else {
1715 ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), 4, $this->colour[$colour]);
1716 ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), 4, $this->colour[$colour]);
1718 break;
1719 case 'open':
1720 ImageLine($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]);
1721 ImageLine($this->image, $u_start, $v_start, $u_start, $zero, $this->colour[$colour]);
1722 ImageLine($this->image, $u_end, $v_end, $u_end, $zero, $this->colour[$colour]);
1723 break;
1725 } else {
1726 $bottom = $this->calculated['boundary_box']['bottom'];
1727 $u_start = $x_start + $offset;
1728 $u_end = $x_end + $offset;
1729 $v_start = $bottom - $y_start + $offset;
1730 $v_end = $bottom - $y_end + $offset;
1732 if ($this->parameter['shadow_below_axis'] ) $bottom += $offset;
1733 if ($this->parameter['inner_border'] != 'none') $bottom -= 1; // 1 pixel above bottom if border is to be drawn.
1734 switch ($type) {
1735 case 'fill':
1736 if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
1737 ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), $this->colour[$colour]);
1738 } else {
1739 ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), 4, $this->colour[$colour]);
1741 break;
1742 case 'open':
1743 if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
1744 ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), $this->colour[$colour]);
1745 } else {
1746 ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), 4, $this->colour[$colour]);
1748 break;
1753 function line($x_start, $y_start, $x_end, $y_end, $type, $brush_type, $brush_size, $colour, $offset) {
1754 //dbug("drawing line of type: $type, at offset: $offset");
1755 $u_start = (int) round($x_start + $offset);
1756 $v_start = (int) round($this->calculated['boundary_box']['bottom'] - $y_start + $offset);
1757 $u_end = (int) round($x_end + $offset);
1758 $v_end = (int) round($this->calculated['boundary_box']['bottom'] - $y_end + $offset);
1760 switch ($type) {
1761 case 'brush':
1762 $this->draw_brush_line($u_start, $v_start, $u_end, $v_end, $brush_size, $brush_type, $colour);
1763 break;
1764 case 'line' :
1765 ImageLine($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]);
1766 break;
1767 case 'dash':
1768 $this->image_dashed_line($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]); // Moodle
1769 break;
1773 // function to draw line. would prefer to use gdBrush but this is not supported yet.
1774 function draw_brush_line($x0, $y0, $x1, $y1, $size, $type, $colour) {
1775 //$this->dbug("line: $x0, $y0, $x1, $y1");
1776 $dy = $y1 - $y0;
1777 $dx = $x1 - $x0;
1778 $t = 0;
1779 $watchdog = 1024; // precaution to prevent infinite loops.
1781 $this->draw_brush($x0, $y0, $size, $type, $colour);
1782 if (abs($dx) > abs($dy)) { // slope < 1
1783 //$this->dbug("slope < 1");
1784 $m = $dy / $dx; // compute slope
1785 $t += $y0;
1786 $dx = ($dx < 0) ? -1 : 1;
1787 $m *= $dx;
1788 while ((int) round($x0) != (int) round($x1)) {
1789 if (!$watchdog--) break;
1790 $x0 += $dx; // step to next x value
1791 $t += $m; // add slope to y value
1792 $y = (int) round($t);
1793 //$this->dbug("x0=$x0, x1=$x1, y=$y watchdog=$watchdog");
1794 $this->draw_brush($x0, $y, $size, $type, $colour);
1797 } else { // slope >= 1
1798 //$this->dbug("slope >= 1");
1799 $m = $dx / $dy; // compute slope
1800 $t += $x0;
1801 $dy = ($dy < 0) ? -1 : 1;
1802 $m *= $dy;
1803 while ((int) round($y0) != (int) round($y1)) {
1804 if (!$watchdog--) break;
1805 $y0 += $dy; // step to next y value
1806 $t += $m; // add slope to x value
1807 $x = (int) round($t);
1808 //$this->dbug("x=$x, y0=$y0, y1=$y1 watchdog=$watchdog");
1809 $this->draw_brush($x, $y0, $size, $type, $colour);
1815 function draw_brush($x, $y, $size, $type, $colour) {
1816 $x = (int) round($x);
1817 $y = (int) round($y);
1818 $half = (int) round($size / 2);
1819 switch ($type) {
1820 case 'circle':
1821 ImageArc($this->image, $x, $y, $size, $size, 0, 360, $this->colour[$colour]);
1822 ImageFillToBorder($this->image, $x, $y, $this->colour[$colour], $this->colour[$colour]);
1823 break;
1824 case 'square':
1825 ImageFilledRectangle($this->image, $x-$half, $y-$half, $x+$half, $y+$half, $this->colour[$colour]);
1826 break;
1827 case 'vertical':
1828 ImageFilledRectangle($this->image, $x, $y-$half, $x+1, $y+$half, $this->colour[$colour]);
1829 break;
1830 case 'horizontal':
1831 ImageFilledRectangle($this->image, $x-$half, $y, $x+$half, $y+1, $this->colour[$colour]);
1832 break;
1833 case 'slash':
1834 if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
1835 ImageFilledPolygon($this->image, array(
1836 $x + $half, $y - $half,
1837 $x + $half + 1, $y - $half,
1838 $x - $half + 1, $y + $half,
1839 $x - $half, $y + $half
1840 ), $this->colour[$colour]);
1841 } else {
1842 ImageFilledPolygon($this->image, array(
1843 $x + $half, $y - $half,
1844 $x + $half + 1, $y - $half,
1845 $x - $half + 1, $y + $half,
1846 $x - $half, $y + $half
1847 ), 4, $this->colour[$colour]);
1849 break;
1850 case 'backslash':
1851 if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
1852 ImageFilledPolygon($this->image, array(
1853 $x - $half, $y - $half,
1854 $x - $half + 1, $y - $half,
1855 $x + $half + 1, $y + $half,
1856 $x + $half, $y + $half
1857 ), $this->colour[$colour]);
1858 } else {
1859 ImageFilledPolygon($this->image, array(
1860 $x - $half, $y-$half,
1861 $x - $half + 1, $y - $half,
1862 $x + $half + 1, $y + $half,
1863 $x + $half, $y + $half
1864 ), 4, $this->colour[$colour]);
1866 break;
1867 default:
1868 @eval($type); // user can create own brush script.
1873 * Moodle.
1875 * A replacement for deprecated ImageDashedLine function.
1877 * @param resource|GdImage $image
1878 * @param int $x1 — x-coordinate for first point.
1879 * @param int $y1 — y-coordinate for first point.
1880 * @param int $x2 — x-coordinate for second point.
1881 * @param int $y2 — y-coordinate for second point.
1882 * @param int $color
1883 * @return void
1885 private function image_dashed_line($image, $x1, $y1, $x2, $y2, $colour): void {
1886 // Create a dashed style.
1887 $style = array(
1888 $colour,
1889 $colour,
1890 $colour,
1891 $colour,
1892 IMG_COLOR_TRANSPARENT,
1893 IMG_COLOR_TRANSPARENT,
1894 IMG_COLOR_TRANSPARENT,
1895 IMG_COLOR_TRANSPARENT
1897 imagesetstyle($image, $style);
1899 // Apply the dashed style.
1900 imageline($image, $x1, $y1, $x2, $y2, IMG_COLOR_STYLED);
1903 } // class graph