Merge branch 'MDL-75945-master' of https://github.com/meirzamoodle/moodle

This commit is contained in:
Andrew Nicols 2023-01-31 22:12:16 +08:00
commit 2b6224a345
3 changed files with 260 additions and 52 deletions

View File

@ -1,5 +1,4 @@
<?php
/**
* Graph Class. PHP Class to draw line, point, bar, and area graphs, including numeric x-axis and double y-axis.
* Version: 1.6.3
@ -26,6 +25,8 @@
* @subpackage lib
*/
declare(strict_types=1);
defined('MOODLE_INTERNAL') || die();
/* This file contains modifications by Martin Dougiamas
@ -539,8 +540,8 @@ class graph {
$tickColour = $this->colour[$this->parameter['x_ticks_colour']];
$axis_colour = $this->parameter['axis_colour'];
$xGrid = $this->parameter['x_grid'];
$gridTop = $this->calculated['boundary_box']['top'];
$gridBottom = $this->calculated['boundary_box']['bottom'];
$gridTop = (int) round($this->calculated['boundary_box']['top']);
$gridBottom = (int) round($this->calculated['boundary_box']['bottom']);
if ($this->parameter['tick_length'] >= 0) {
$tickTop = $this->calculated['boundary_box']['bottom'] - $this->parameter['tick_length'];
@ -565,14 +566,15 @@ class graph {
$axisTag = array('points' => $axis_size, 'angle' => $axis_angle, 'font' => $axis_font, 'colour' => $axis_colour);
foreach ($this->calculated['x_axis']['tick_x'] as $set => $tickX) {
$tickX = (int) round($tickX);
// draw x grid if colour specified
if ($xGrid != 'none') {
switch ($xGrid) {
case 'line':
ImageLine($this->image, round($tickX), round($gridTop), round($tickX), round($gridBottom), $gridColour);
ImageLine($this->image, $tickX, $gridTop, $tickX, $gridBottom, $gridColour);
break;
case 'dash':
ImageDashedLine($this->image, round($tickX), round($gridTop), round($tickX), round($gridBottom), $gridColour);
$this->image_dashed_line($this->image, $tickX, $gridTop, $tickX, $gridBottom, $gridColour); // Moodle
break;
}
}
@ -580,7 +582,7 @@ class graph {
if ($this->parameter['x_axis_text'] && !($set % $this->parameter['x_axis_text'])) { // test if tick should be displayed
// draw tick
if ($tickColour != 'none')
ImageLine($this->image, round($tickX), round($tickTop), round($tickX), round($tickBottom), $tickColour);
ImageLine($this->image, $tickX, $tickTop, $tickX, $tickBottom, $tickColour);
// draw axis text
$coords = array('x' => $tickX, 'y' => $textBottom, 'reference' => $reference);
@ -597,8 +599,8 @@ class graph {
$tickColour = $this->colour[$this->parameter['y_ticks_colour']];
$axis_colour = $this->parameter['axis_colour'];
$yGrid = $this->parameter['y_grid'];
$gridLeft = $this->calculated['boundary_box']['left'];
$gridRight = $this->calculated['boundary_box']['right'];
$gridLeft = (int) round($this->calculated['boundary_box']['left']);
$gridRight = (int) round($this->calculated['boundary_box']['right']);
// axis font information
$axis_font = $this->parameter['axis_font'];
@ -625,14 +627,15 @@ class graph {
if ($axis_angle == 90) $reference = 'right-center';
foreach ($this->calculated['y_axis']['tick_y'] as $set => $tickY) {
$tickY = (int) round($tickY);
// draw y grid if colour specified
if ($yGrid != 'none') {
switch ($yGrid) {
case 'line':
ImageLine($this->image, round($gridLeft), round($tickY), round($gridRight), round($tickY), $gridColour);
ImageLine($this->image, $gridLeft, $tickY, $gridRight, $tickY, $gridColour);
break;
case 'dash':
ImageDashedLine($this->image, round($gridLeft), round($tickY), round($gridRight), round($tickY), $gridColour);
$this->image_dashed_line($this->image, $gridLeft, $tickY, $gridRight, $tickY, $gridColour); // Moodle
break;
}
}
@ -641,7 +644,7 @@ class graph {
if ($this->parameter['y_axis_text_left'] && !($set % $this->parameter['y_axis_text_left'])) { // test if tick should be displayed
// draw tick
if ($tickColour != 'none')
ImageLine($this->image, round($tickLeft), round($tickY), round($tickRight), round($tickY), $tickColour);
ImageLine($this->image, $tickLeft, $tickY, $tickRight, $tickY, $tickColour);
// draw axis text...
$coords = array('x' => $textRight, 'y' => $tickY, 'reference' => $reference);
@ -677,7 +680,7 @@ class graph {
ImageLine($this->image, round($gridLeft), round($tickY), round($gridRight), round($tickY), $gridColour);
break;
case 'dash':
ImageDashedLine($this->image, round($gridLeft), round($tickY), round($gridRight), round($tickY), $gridColour);
$this->image_dashed_line($this->image, round($gridLeft), round($tickY), round($gridRight), round($tickY), $gridColour); // Moodle
break;
}
}
@ -1275,6 +1278,7 @@ class graph {
// start of Moodle addition
$text = core_text::utf8_to_entities($text, true, true); //does not work with hex entities!
// end of Moodle addition
[$x, $y] = [(int) round($x), (int) round($y)];
ImageTTFText($this->image, $points, $angle, $x, $y, $colour, $font, $text);
}
@ -1549,7 +1553,7 @@ class graph {
$u = $x + $offset;
$v = $this->calculated['inner_border']['bottom'] - $y + $offset;
$half = $size / 2;
[$u, $v, $half] = [(int) round($u), (int) round($v), (int) round($half)];
switch ($type) {
case 'square':
ImageFilledRectangle($this->image, $u-$half, $v-$half, $u+$half, $v+$half, $this->colour[$colour]);
@ -1565,16 +1569,32 @@ class graph {
ImageArc($this->image, $u, $v, $size, $size, 0, 360, $this->colour[$colour]);
break;
case 'diamond':
ImageFilledPolygon($this->image, array($u, $v-$half, $u+$half, $v, $u, $v+$half, $u-$half, $v), 4, $this->colour[$colour]);
if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
ImageFilledPolygon($this->image, array($u, $v - $half, $u + $half, $v, $u, $v + $half, $u - $half, $v), $this->colour[$colour]);
} else {
ImageFilledPolygon($this->image, array($u, $v - $half, $u + $half, $v, $u, $v + $half, $u - $half, $v), 4, $this->colour[$colour]);
}
break;
case 'diamond-open':
ImagePolygon($this->image, array($u, $v-$half, $u+$half, $v, $u, $v+$half, $u-$half, $v), 4, $this->colour[$colour]);
if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
ImagePolygon($this->image, array($u, $v - $half, $u + $half, $v, $u, $v + $half, $u - $half, $v), $this->colour[$colour]);
} else {
ImagePolygon($this->image, array($u, $v - $half, $u + $half, $v, $u, $v + $half, $u - $half, $v), 4, $this->colour[$colour]);
}
break;
case 'triangle':
ImageFilledPolygon($this->image, array($u, $v-$half, $u+$half, $v+$half, $u-$half, $v+$half), 3, $this->colour[$colour]);
if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
ImageFilledPolygon($this->image, array($u, $v - $half, $u + $half, $v + $half, $u - $half, $v + $half), $this->colour[$colour]);
} else {
ImageFilledPolygon($this->image, array($u, $v - $half, $u + $half, $v + $half, $u - $half, $v + $half), 3, $this->colour[$colour]);
}
break;
case 'triangle-open':
ImagePolygon($this->image, array($u, $v-$half, $u+$half, $v+$half, $u-$half, $v+$half), 3, $this->colour[$colour]);
if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
ImagePolygon($this->image, array($u, $v - $half, $u + $half, $v + $half, $u - $half, $v + $half), $this->colour[$colour]);
} else {
ImagePolygon($this->image, array($u, $v - $half, $u + $half, $v + $half, $u - $half, $v + $half), 3, $this->colour[$colour]);
}
break;
case 'dot':
ImageSetPixel($this->image, $u, $v, $this->colour[$colour]);
@ -1598,8 +1618,8 @@ class graph {
if ($this->parameter['zero_axis'] != 'none') {
$zero = $this->calculated['zero_axis'];
if ($this->parameter['shadow_below_axis'] ) $zero += $offset;
$u_left = $x_left + $offset;
$u_right = $x_right + $offset - 1;
$u_left = (int) round($x_left + $offset);
$u_right = (int) round($x_right + $offset - 1);
$v = $this->calculated['boundary_box']['bottom'] - $y + $offset;
if ($v > $zero) {
@ -1610,18 +1630,19 @@ class graph {
$bottom = $zero - 1;
}
[$top, $bottom] = [(int) round($top), (int) round($bottom)];
switch ($type) {
case 'open':
//ImageRectangle($this->image, round($u_left), $top, round($u_right), $bottom, $this->colour[$colour]);
if ($v > $zero)
ImageRectangle($this->image, round($u_left), $bottom, round($u_right), $bottom, $this->colour[$colour]);
ImageRectangle($this->image, $u_left, $bottom, $u_right, $bottom, $this->colour[$colour]);
else
ImageRectangle($this->image, round($u_left), $top, round($u_right), $top, $this->colour[$colour]);
ImageRectangle($this->image, round($u_left), $top, round($u_left), $bottom, $this->colour[$colour]);
ImageRectangle($this->image, round($u_right), $top, round($u_right), $bottom, $this->colour[$colour]);
ImageRectangle($this->image, $u_left, $top, $u_right, $top, $this->colour[$colour]);
ImageRectangle($this->image, $u_left, $top, $u_left, $bottom, $this->colour[$colour]);
ImageRectangle($this->image, $u_right, $top, $u_right, $bottom, $this->colour[$colour]);
break;
case 'fill':
ImageFilledRectangle($this->image, round($u_left), $top, round($u_right), $bottom, $this->colour[$colour]);
ImageFilledRectangle($this->image, $u_left, $top, $u_right, $bottom, $this->colour[$colour]);
break;
}
@ -1630,8 +1651,8 @@ class graph {
$bottom = $this->calculated['boundary_box']['bottom'];
if ($this->parameter['shadow_below_axis'] ) $bottom += $offset;
if ($this->parameter['inner_border'] != 'none') $bottom -= 1; // 1 pixel above bottom if border is to be drawn.
$u_left = $x_left + $offset;
$u_right = $x_right + $offset - 1;
$u_left = (int) round($x_left + $offset);
$u_right = (int) round($x_right + $offset - 1);
$v = $this->calculated['boundary_box']['bottom'] - $y + $offset;
// Moodle addition, plus the function parameter yoffset
@ -1641,12 +1662,14 @@ class graph {
$v -= $yoffset; // Moodle
} // Moodle
[$v, $bottom] = [(int) round($v), (int) round($bottom)];
switch ($type) {
case 'open':
ImageRectangle($this->image, round($u_left), $v, round($u_right), $bottom, $this->colour[$colour]);
ImageRectangle($this->image, $u_left, $v, $u_right, $bottom, $this->colour[$colour]);
break;
case 'fill':
ImageFilledRectangle($this->image, round($u_left), $v, round($u_right), $bottom, $this->colour[$colour]);
ImageFilledRectangle($this->image, $u_left, $v, $u_right, $bottom, $this->colour[$colour]);
break;
}
}
@ -1665,11 +1688,15 @@ class graph {
switch ($type) {
case 'fill':
// draw it this way 'cos the FilledPolygon routine seems a bit buggy.
ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), 4, $this->colour[$colour]);
ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), 4, $this->colour[$colour]);
break;
if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), $this->colour[$colour]);
ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), $this->colour[$colour]);
} else {
ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), 4, $this->colour[$colour]);
ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), 4, $this->colour[$colour]);
}
break;
case 'open':
//ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $zero, $u_start, $zero), 4, $this->colour[$colour]);
ImageLine($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]);
ImageLine($this->image, $u_start, $v_start, $u_start, $zero, $this->colour[$colour]);
ImageLine($this->image, $u_end, $v_end, $u_end, $zero, $this->colour[$colour]);
@ -1686,21 +1713,29 @@ class graph {
if ($this->parameter['inner_border'] != 'none') $bottom -= 1; // 1 pixel above bottom if border is to be drawn.
switch ($type) {
case 'fill':
ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), 4, $this->colour[$colour]);
break;
case 'open':
ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), 4, $this->colour[$colour]);
if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), $this->colour[$colour]);
} else {
ImageFilledPolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), 4, $this->colour[$colour]);
}
break;
case 'open':
if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), $this->colour[$colour]);
} else {
ImagePolygon($this->image, array($u_start, $v_start, $u_end, $v_end, $u_end, $bottom, $u_start, $bottom), 4, $this->colour[$colour]);
}
break;
}
}
}
function line($x_start, $y_start, $x_end, $y_end, $type, $brush_type, $brush_size, $colour, $offset) {
//dbug("drawing line of type: $type, at offset: $offset");
$u_start = $x_start + $offset;
$v_start = $this->calculated['boundary_box']['bottom'] - $y_start + $offset;
$u_end = $x_end + $offset;
$v_end = $this->calculated['boundary_box']['bottom'] - $y_end + $offset;
$u_start = (int) round($x_start + $offset);
$v_start = (int) round($this->calculated['boundary_box']['bottom'] - $y_start + $offset);
$u_end = (int) round($x_end + $offset);
$v_end = (int) round($this->calculated['boundary_box']['bottom'] - $y_end + $offset);
switch ($type) {
case 'brush':
@ -1710,7 +1745,7 @@ class graph {
ImageLine($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]);
break;
case 'dash':
ImageDashedLine($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]);
$this->image_dashed_line($this->image, $u_start, $v_start, $u_end, $v_end, $this->colour[$colour]); // Moodle
break;
}
}
@ -1776,22 +1811,73 @@ class graph {
ImageFilledRectangle($this->image, $x-$half, $y, $x+$half, $y+1, $this->colour[$colour]);
break;
case 'slash':
ImageFilledPolygon($this->image, array($x+$half, $y-$half,
$x+$half+1, $y-$half,
$x-$half+1, $y+$half,
$x-$half, $y+$half
), 4, $this->colour[$colour]);
if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
ImageFilledPolygon($this->image, array(
$x + $half, $y - $half,
$x + $half + 1, $y - $half,
$x - $half + 1, $y + $half,
$x - $half, $y + $half
), $this->colour[$colour]);
} else {
ImageFilledPolygon($this->image, array(
$x + $half, $y - $half,
$x + $half + 1, $y - $half,
$x - $half + 1, $y + $half,
$x - $half, $y + $half
), 4, $this->colour[$colour]);
}
break;
case 'backslash':
ImageFilledPolygon($this->image, array($x-$half, $y-$half,
$x-$half+1, $y-$half,
$x+$half+1, $y+$half,
$x+$half, $y+$half
), 4, $this->colour[$colour]);
if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
ImageFilledPolygon($this->image, array(
$x - $half, $y - $half,
$x - $half + 1, $y - $half,
$x + $half + 1, $y + $half,
$x + $half, $y + $half
), $this->colour[$colour]);
} else {
ImageFilledPolygon($this->image, array(
$x - $half, $y-$half,
$x - $half + 1, $y - $half,
$x + $half + 1, $y + $half,
$x + $half, $y + $half
), 4, $this->colour[$colour]);
}
break;
default:
@eval($type); // user can create own brush script.
}
}
/**
* Moodle.
*
* A replacement for deprecated ImageDashedLine function.
*
* @param resource|GdImage $image
* @param int $x1 x-coordinate for first point.
* @param int $y1 y-coordinate for first point.
* @param int $x2 x-coordinate for second point.
* @param int $y2 y-coordinate for second point.
* @param int $color
* @return void
*/
private function image_dashed_line($image, $x1, $y1, $x2, $y2, $colour): void {
// Create a dashed style.
$style = array(
$colour,
$colour,
$colour,
$colour,
IMG_COLOR_TRANSPARENT,
IMG_COLOR_TRANSPARENT,
IMG_COLOR_TRANSPARENT,
IMG_COLOR_TRANSPARENT
);
imagesetstyle($image, $style);
// Apply the dashed style.
imageline($image, $x1, $y1, $x2, $y2, IMG_COLOR_STYLED);
}
} // class graph

121
lib/tests/graphlib_test.php Normal file
View File

@ -0,0 +1,121 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once("$CFG->libdir/graphlib.php");
/**
* Tests for Graphlib.
*
* @coversDefaultClass \graph
* @package core
* @copyright 2023 Meirza (meirza.arson@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class graphlib_test extends \basic_testcase {
/**
* Data provider for test_graphlib.
*
* @return array
*/
public function create_data(): array {
return [
'data' =>
[
'mock' => [
'survey_name' => 'Survey 101',
'names' => [
'Relevance', 'Reflective thinking', 'Interactivity', 'Tutor support', 'Peer support', 'Interpretation'
],
'buckets1' => [
0.75, 2.5, 1.5, 1.5, 2.5, 2.5
],
'stractual' => 'Actual',
'buckets2' => [
-1, -1, -1, -1, -1, -1
],
'strpreferred' => 'Preferred',
'stdev1' => [
0.82915619758885, 1.1180339887499, 1.1180339887499, 1.1180339887499, 1.1180339887499, 1.1180339887499
],
'stdev2' => [
0, 0, 0, 0, 0, 0
],
'options' => [
'Almost never', 'Seldom', 'Sometimes', 'Often', 'Almost always'
],
'maxbuckets1' => 2.5,
'maxbuckets2' => -1
]
]
];
}
/**
* Test graphlib.
*
* @dataProvider create_data
* @covers ::output
* @param array $mock
* @return void
*/
public function test_graphlib($mock) {
$graph = new \graph(300, 200);
ob_start();
$graph->parameter['title'] = strip_tags(format_string($mock['survey_name'], true));
$graph->x_data = $mock['names'];
$graph->y_data['answers1'] = $mock['buckets1'];
$graph->y_format['answers1'] = array('colour' => 'ltblue', 'line' => 'line', 'point' => 'square',
'shadow_offset' => 4, 'legend' => $mock['stractual']);
$graph->y_data['answers2'] = $mock['buckets2'];
$graph->y_format['answers2'] = array('colour' => 'ltorange', 'line' => 'line', 'point' => 'square',
'shadow_offset' => 4, 'legend' => $mock['strpreferred']);
$graph->y_data['stdev1'] = $mock['stdev1'];
$graph->y_format['stdev1'] = array('colour' => 'ltltblue', 'bar' => 'fill',
'shadow_offset' => '4', 'legend' => 'none', 'bar_size' => 0.3);
$graph->y_data['stdev2'] = $mock['stdev2'];
$graph->y_format['stdev2'] = array('colour' => 'ltltorange', 'bar' => 'fill',
'shadow_offset' => '4', 'legend' => 'none', 'bar_size' => 0.2);
$graph->offset_relation['stdev1'] = 'answers1';
$graph->offset_relation['stdev2'] = 'answers2';
$graph->parameter['legend'] = 'outside-top';
$graph->parameter['legend_border'] = 'black';
$graph->parameter['legend_offset'] = 4;
$graph->y_tick_labels = $mock['options'];
if (($mock['maxbuckets1'] > 0.0) && ($mock['maxbuckets2'] > 0.0)) {
$graph->y_order = array('stdev1', 'answers1', 'stdev2', 'answers2');
} else if ($mock['maxbuckets1'] > 0.0) {
$graph->y_order = array('stdev1', 'answers1');
} else {
$graph->y_order = array('stdev2', 'answers2');
}
$graph->parameter['y_max_left'] = 4;
$graph->parameter['y_axis_gridlines'] = 5;
$graph->parameter['y_resolution_left'] = 1;
$graph->parameter['y_decimal_left'] = 1;
$graph->parameter['x_axis_angle'] = 0;
$graph->parameter['x_inner_padding'] = 6;
@$graph->draw();
$res = ob_end_clean();
$this->assertTrue($res);
}
}

View File

@ -56,6 +56,7 @@ information provided here is intended especially for developers.
The old class locations have been aliased for backwards compatibility and will emit a deprecation notice in a future
release.
* Convert a floating value to an integer in lib/graphlib.php to avoid PHP 8.1 deprecated function error.
=== 4.1 ===