Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 401 and 403] [Versions 402 and 403]

   1  <?php
   2  
   3  namespace PhpOffice\PhpSpreadsheet\Chart\Renderer;
   4  
   5  use AccBarPlot;
   6  use AccLinePlot;
   7  use BarPlot;
   8  use ContourPlot;
   9  use Graph;
  10  use GroupBarPlot;
  11  use LinePlot;
  12  use PhpOffice\PhpSpreadsheet\Chart\Chart;
  13  use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
  14  use PieGraph;
  15  use PiePlot;
  16  use PiePlot3D;
  17  use PiePlotC;
  18  use RadarGraph;
  19  use RadarPlot;
  20  use ScatterPlot;
  21  use Spline;
  22  use StockPlot;
  23  
  24  /**
  25   * Base class for different Jpgraph implementations as charts renderer.
  26   */
  27  abstract class JpGraphRendererBase implements IRenderer
  28  {
  29      private static $width = 640;
  30  
  31      private static $height = 480;
  32  
  33      private static $colourSet = [
  34          'mediumpurple1', 'palegreen3', 'gold1', 'cadetblue1',
  35          'darkmagenta', 'coral', 'dodgerblue3', 'eggplant',
  36          'mediumblue', 'magenta', 'sandybrown', 'cyan',
  37          'firebrick1', 'forestgreen', 'deeppink4', 'darkolivegreen',
  38          'goldenrod2',
  39      ];
  40  
  41      private static $markSet;
  42  
  43      private $chart;
  44  
  45      private $graph;
  46  
  47      private static $plotColour = 0;
  48  
  49      private static $plotMark = 0;
  50  
  51      /**
  52       * Create a new jpgraph.
  53       */
  54      public function __construct(Chart $chart)
  55      {
  56          static::init();
  57          $this->graph = null;
  58          $this->chart = $chart;
  59  
  60          self::$markSet = [
  61              'diamond' => MARK_DIAMOND,
  62              'square' => MARK_SQUARE,
  63              'triangle' => MARK_UTRIANGLE,
  64              'x' => MARK_X,
  65              'star' => MARK_STAR,
  66              'dot' => MARK_FILLEDCIRCLE,
  67              'dash' => MARK_DTRIANGLE,
  68              'circle' => MARK_CIRCLE,
  69              'plus' => MARK_CROSS,
  70          ];
  71      }
  72  
  73      /**
  74       * This method should be overriden in descendants to do real JpGraph library initialization.
  75       */
  76      abstract protected static function init(): void;
  77  
  78      private function formatPointMarker($seriesPlot, $markerID)
  79      {
  80          $plotMarkKeys = array_keys(self::$markSet);
  81          if ($markerID === null) {
  82              //    Use default plot marker (next marker in the series)
  83              self::$plotMark %= count(self::$markSet);
  84              $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]);
  85          } elseif ($markerID !== 'none') {
  86              //    Use specified plot marker (if it exists)
  87              if (isset(self::$markSet[$markerID])) {
  88                  $seriesPlot->mark->SetType(self::$markSet[$markerID]);
  89              } else {
  90                  //    If the specified plot marker doesn't exist, use default plot marker (next marker in the series)
  91                  self::$plotMark %= count(self::$markSet);
  92                  $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]);
  93              }
  94          } else {
  95              //    Hide plot marker
  96              $seriesPlot->mark->Hide();
  97          }
  98          $seriesPlot->mark->SetColor(self::$colourSet[self::$plotColour]);
  99          $seriesPlot->mark->SetFillColor(self::$colourSet[self::$plotColour]);
 100          $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]);
 101  
 102          return $seriesPlot;
 103      }
 104  
 105      private function formatDataSetLabels($groupID, $datasetLabels, $rotation = '')
 106      {
 107          $datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode() ?? '';
 108          //    Retrieve any label formatting code
 109          $datasetLabelFormatCode = stripslashes($datasetLabelFormatCode);
 110  
 111          $testCurrentIndex = 0;
 112          foreach ($datasetLabels as $i => $datasetLabel) {
 113              if (is_array($datasetLabel)) {
 114                  if ($rotation == 'bar') {
 115                      $datasetLabels[$i] = implode(' ', $datasetLabel);
 116                  } else {
 117                      $datasetLabel = array_reverse($datasetLabel);
 118                      $datasetLabels[$i] = implode("\n", $datasetLabel);
 119                  }
 120              } else {
 121                  //    Format labels according to any formatting code
 122                  if ($datasetLabelFormatCode !== null) {
 123                      $datasetLabels[$i] = NumberFormat::toFormattedString($datasetLabel, $datasetLabelFormatCode);
 124                  }
 125              }
 126              ++$testCurrentIndex;
 127          }
 128  
 129          return $datasetLabels;
 130      }
 131  
 132      private function percentageSumCalculation($groupID, $seriesCount)
 133      {
 134          $sumValues = [];
 135          //    Adjust our values to a percentage value across all series in the group
 136          for ($i = 0; $i < $seriesCount; ++$i) {
 137              if ($i == 0) {
 138                  $sumValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
 139              } else {
 140                  $nextValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
 141                  foreach ($nextValues as $k => $value) {
 142                      if (isset($sumValues[$k])) {
 143                          $sumValues[$k] += $value;
 144                      } else {
 145                          $sumValues[$k] = $value;
 146                      }
 147                  }
 148              }
 149          }
 150  
 151          return $sumValues;
 152      }
 153  
 154      private function percentageAdjustValues($dataValues, $sumValues)
 155      {
 156          foreach ($dataValues as $k => $dataValue) {
 157              $dataValues[$k] = $dataValue / $sumValues[$k] * 100;
 158          }
 159  
 160          return $dataValues;
 161      }
 162  
 163      private function getCaption($captionElement)
 164      {
 165          //    Read any caption
 166          $caption = ($captionElement !== null) ? $captionElement->getCaption() : null;
 167          //    Test if we have a title caption to display
 168          if ($caption !== null) {
 169              //    If we do, it could be a plain string or an array
 170              if (is_array($caption)) {
 171                  //    Implode an array to a plain string
 172                  $caption = implode('', $caption);
 173              }
 174          }
 175  
 176          return $caption;
 177      }
 178  
 179      private function renderTitle(): void
 180      {
 181          $title = $this->getCaption($this->chart->getTitle());
 182          if ($title !== null) {
 183              $this->graph->title->Set($title);
 184          }
 185      }
 186  
 187      private function renderLegend(): void
 188      {
 189          $legend = $this->chart->getLegend();
 190          if ($legend !== null) {
 191              $legendPosition = $legend->getPosition();
 192              switch ($legendPosition) {
 193                  case 'r':
 194                      $this->graph->legend->SetPos(0.01, 0.5, 'right', 'center'); //    right
 195                      $this->graph->legend->SetColumns(1);
 196  
 197                      break;
 198                  case 'l':
 199                      $this->graph->legend->SetPos(0.01, 0.5, 'left', 'center'); //    left
 200                      $this->graph->legend->SetColumns(1);
 201  
 202                      break;
 203                  case 't':
 204                      $this->graph->legend->SetPos(0.5, 0.01, 'center', 'top'); //    top
 205  
 206                      break;
 207                  case 'b':
 208                      $this->graph->legend->SetPos(0.5, 0.99, 'center', 'bottom'); //    bottom
 209  
 210                      break;
 211                  default:
 212                      $this->graph->legend->SetPos(0.01, 0.01, 'right', 'top'); //    top-right
 213                      $this->graph->legend->SetColumns(1);
 214  
 215                      break;
 216              }
 217          } else {
 218              $this->graph->legend->Hide();
 219          }
 220      }
 221  
 222      private function renderCartesianPlotArea($type = 'textlin'): void
 223      {
 224          $this->graph = new Graph(self::$width, self::$height);
 225          $this->graph->SetScale($type);
 226  
 227          $this->renderTitle();
 228  
 229          //    Rotate for bar rather than column chart
 230          $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotDirection();
 231          $reverse = $rotation == 'bar';
 232  
 233          $xAxisLabel = $this->chart->getXAxisLabel();
 234          if ($xAxisLabel !== null) {
 235              $title = $this->getCaption($xAxisLabel);
 236              if ($title !== null) {
 237                  $this->graph->xaxis->SetTitle($title, 'center');
 238                  $this->graph->xaxis->title->SetMargin(35);
 239                  if ($reverse) {
 240                      $this->graph->xaxis->title->SetAngle(90);
 241                      $this->graph->xaxis->title->SetMargin(90);
 242                  }
 243              }
 244          }
 245  
 246          $yAxisLabel = $this->chart->getYAxisLabel();
 247          if ($yAxisLabel !== null) {
 248              $title = $this->getCaption($yAxisLabel);
 249              if ($title !== null) {
 250                  $this->graph->yaxis->SetTitle($title, 'center');
 251                  if ($reverse) {
 252                      $this->graph->yaxis->title->SetAngle(0);
 253                      $this->graph->yaxis->title->SetMargin(-55);
 254                  }
 255              }
 256          }
 257      }
 258  
 259      private function renderPiePlotArea(): void
 260      {
 261          $this->graph = new PieGraph(self::$width, self::$height);
 262  
 263          $this->renderTitle();
 264      }
 265  
 266      private function renderRadarPlotArea(): void
 267      {
 268          $this->graph = new RadarGraph(self::$width, self::$height);
 269          $this->graph->SetScale('lin');
 270  
 271          $this->renderTitle();
 272      }
 273  
 274      private function renderPlotLine($groupID, $filled = false, $combination = false): void
 275      {
 276          $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping();
 277  
 278          $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[0];
 279          $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount();
 280          if ($labelCount > 0) {
 281              $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues();
 282              $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels);
 283              $this->graph->xaxis->SetTickLabels($datasetLabels);
 284          }
 285  
 286          $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
 287          $seriesPlots = [];
 288          if ($grouping == 'percentStacked') {
 289              $sumValues = $this->percentageSumCalculation($groupID, $seriesCount);
 290          } else {
 291              $sumValues = [];
 292          }
 293  
 294          //    Loop through each data series in turn
 295          for ($i = 0; $i < $seriesCount; ++$i) {
 296              $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[$i];
 297              $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getDataValues();
 298              $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointMarker();
 299  
 300              if ($grouping == 'percentStacked') {
 301                  $dataValues = $this->percentageAdjustValues($dataValues, $sumValues);
 302              }
 303  
 304              //    Fill in any missing values in the $dataValues array
 305              $testCurrentIndex = 0;
 306              foreach ($dataValues as $k => $dataValue) {
 307                  while ($k != $testCurrentIndex) {
 308                      $dataValues[$testCurrentIndex] = null;
 309                      ++$testCurrentIndex;
 310                  }
 311                  ++$testCurrentIndex;
 312              }
 313  
 314              $seriesPlot = new LinePlot($dataValues);
 315              if ($combination) {
 316                  $seriesPlot->SetBarCenter();
 317              }
 318  
 319              if ($filled) {
 320                  $seriesPlot->SetFilled(true);
 321                  $seriesPlot->SetColor('black');
 322                  $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]);
 323              } else {
 324                  //    Set the appropriate plot marker
 325                  $this->formatPointMarker($seriesPlot, $marker);
 326              }
 327              $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($index)->getDataValue();
 328              $seriesPlot->SetLegend($dataLabel);
 329  
 330              $seriesPlots[] = $seriesPlot;
 331          }
 332  
 333          if ($grouping == 'standard') {
 334              $groupPlot = $seriesPlots;
 335          } else {
 336              $groupPlot = new AccLinePlot($seriesPlots);
 337          }
 338          $this->graph->Add($groupPlot);
 339      }
 340  
 341      private function renderPlotBar($groupID, $dimensions = '2d'): void
 342      {
 343          $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotDirection();
 344          //    Rotate for bar rather than column chart
 345          if (($groupID == 0) && ($rotation == 'bar')) {
 346              $this->graph->Set90AndMargin();
 347          }
 348          $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping();
 349  
 350          $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[0];
 351          $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount();
 352          if ($labelCount > 0) {
 353              $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues();
 354              $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $rotation);
 355              //    Rotate for bar rather than column chart
 356              if ($rotation == 'bar') {
 357                  $datasetLabels = array_reverse($datasetLabels);
 358                  $this->graph->yaxis->SetPos('max');
 359                  $this->graph->yaxis->SetLabelAlign('center', 'top');
 360                  $this->graph->yaxis->SetLabelSide(SIDE_RIGHT);
 361              }
 362              $this->graph->xaxis->SetTickLabels($datasetLabels);
 363          }
 364  
 365          $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
 366          $seriesPlots = [];
 367          if ($grouping == 'percentStacked') {
 368              $sumValues = $this->percentageSumCalculation($groupID, $seriesCount);
 369          } else {
 370              $sumValues = [];
 371          }
 372  
 373          //    Loop through each data series in turn
 374          for ($j = 0; $j < $seriesCount; ++$j) {
 375              $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[$j];
 376              $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getDataValues();
 377              if ($grouping == 'percentStacked') {
 378                  $dataValues = $this->percentageAdjustValues($dataValues, $sumValues);
 379              }
 380  
 381              //    Fill in any missing values in the $dataValues array
 382              $testCurrentIndex = 0;
 383              foreach ($dataValues as $k => $dataValue) {
 384                  while ($k != $testCurrentIndex) {
 385                      $dataValues[$testCurrentIndex] = null;
 386                      ++$testCurrentIndex;
 387                  }
 388                  ++$testCurrentIndex;
 389              }
 390  
 391              //    Reverse the $dataValues order for bar rather than column chart
 392              if ($rotation == 'bar') {
 393                  $dataValues = array_reverse($dataValues);
 394              }
 395              $seriesPlot = new BarPlot($dataValues);
 396              $seriesPlot->SetColor('black');
 397              $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]);
 398              if ($dimensions == '3d') {
 399                  $seriesPlot->SetShadow();
 400              }
 401              if (!$this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)) {
 402                  $dataLabel = '';
 403              } else {
 404                  $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)->getDataValue();
 405              }
 406              $seriesPlot->SetLegend($dataLabel);
 407  
 408              $seriesPlots[] = $seriesPlot;
 409          }
 410          //    Reverse the plot order for bar rather than column chart
 411          if (($rotation == 'bar') && ($grouping != 'percentStacked')) {
 412              $seriesPlots = array_reverse($seriesPlots);
 413          }
 414  
 415          if ($grouping == 'clustered') {
 416              $groupPlot = new GroupBarPlot($seriesPlots);
 417          } elseif ($grouping == 'standard') {
 418              $groupPlot = new GroupBarPlot($seriesPlots);
 419          } else {
 420              $groupPlot = new AccBarPlot($seriesPlots);
 421              if ($dimensions == '3d') {
 422                  $groupPlot->SetShadow();
 423              }
 424          }
 425  
 426          $this->graph->Add($groupPlot);
 427      }
 428  
 429      private function renderPlotScatter($groupID, $bubble): void
 430      {
 431          $scatterStyle = $bubbleSize = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle();
 432  
 433          $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
 434  
 435          //    Loop through each data series in turn
 436          for ($i = 0; $i < $seriesCount; ++$i) {
 437              $plotCategoryByIndex = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i);
 438              if ($plotCategoryByIndex === false) {
 439                  $plotCategoryByIndex = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0);
 440              }
 441              $dataValuesY = $plotCategoryByIndex->getDataValues();
 442              $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
 443  
 444              $redoDataValuesY = true;
 445              if ($bubble) {
 446                  if (!$bubbleSize) {
 447                      $bubbleSize = '10';
 448                  }
 449                  $redoDataValuesY = false;
 450                  foreach ($dataValuesY as $dataValueY) {
 451                      if (!is_int($dataValueY) && !is_float($dataValueY)) {
 452                          $redoDataValuesY = true;
 453  
 454                          break;
 455                      }
 456                  }
 457              }
 458              if ($redoDataValuesY) {
 459                  foreach ($dataValuesY as $k => $dataValueY) {
 460                      $dataValuesY[$k] = $k;
 461                  }
 462              }
 463              //var_dump($dataValuesY, $dataValuesX, $bubbleSize);
 464  
 465              $seriesPlot = new ScatterPlot($dataValuesX, $dataValuesY);
 466              if ($scatterStyle == 'lineMarker') {
 467                  $seriesPlot->SetLinkPoints();
 468                  $seriesPlot->link->SetColor(self::$colourSet[self::$plotColour]);
 469              } elseif ($scatterStyle == 'smoothMarker') {
 470                  $spline = new Spline($dataValuesY, $dataValuesX);
 471                  [$splineDataY, $splineDataX] = $spline->Get(count($dataValuesX) * self::$width / 20);
 472                  $lplot = new LinePlot($splineDataX, $splineDataY);
 473                  $lplot->SetColor(self::$colourSet[self::$plotColour]);
 474  
 475                  $this->graph->Add($lplot);
 476              }
 477  
 478              if ($bubble) {
 479                  $this->formatPointMarker($seriesPlot, 'dot');
 480                  $seriesPlot->mark->SetColor('black');
 481                  $seriesPlot->mark->SetSize($bubbleSize);
 482              } else {
 483                  $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker();
 484                  $this->formatPointMarker($seriesPlot, $marker);
 485              }
 486              $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue();
 487              $seriesPlot->SetLegend($dataLabel);
 488  
 489              $this->graph->Add($seriesPlot);
 490          }
 491      }
 492  
 493      private function renderPlotRadar($groupID): void
 494      {
 495          $radarStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle();
 496  
 497          $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
 498  
 499          //    Loop through each data series in turn
 500          for ($i = 0; $i < $seriesCount; ++$i) {
 501              $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues();
 502              $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
 503              $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker();
 504  
 505              $dataValues = [];
 506              foreach ($dataValuesY as $k => $dataValueY) {
 507                  $dataValues[$k] = is_array($dataValueY) ? implode(' ', array_reverse($dataValueY)) : $dataValueY;
 508              }
 509              $tmp = array_shift($dataValues);
 510              $dataValues[] = $tmp;
 511              $tmp = array_shift($dataValuesX);
 512              $dataValuesX[] = $tmp;
 513  
 514              $this->graph->SetTitles(array_reverse($dataValues));
 515  
 516              $seriesPlot = new RadarPlot(array_reverse($dataValuesX));
 517  
 518              $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue();
 519              $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]);
 520              if ($radarStyle == 'filled') {
 521                  $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour]);
 522              }
 523              $this->formatPointMarker($seriesPlot, $marker);
 524              $seriesPlot->SetLegend($dataLabel);
 525  
 526              $this->graph->Add($seriesPlot);
 527          }
 528      }
 529  
 530      private function renderPlotContour($groupID): void
 531      {
 532          $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
 533  
 534          $dataValues = [];
 535          //    Loop through each data series in turn
 536          for ($i = 0; $i < $seriesCount; ++$i) {
 537              $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
 538  
 539              $dataValues[$i] = $dataValuesX;
 540          }
 541          $seriesPlot = new ContourPlot($dataValues);
 542  
 543          $this->graph->Add($seriesPlot);
 544      }
 545  
 546      private function renderPlotStock($groupID): void
 547      {
 548          $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
 549          $plotOrder = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder();
 550  
 551          $dataValues = [];
 552          //    Loop through each data series in turn and build the plot arrays
 553          foreach ($plotOrder as $i => $v) {
 554              $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v);
 555              if ($dataValuesX === false) {
 556                  continue;
 557              }
 558              $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v)->getDataValues();
 559              foreach ($dataValuesX as $j => $dataValueX) {
 560                  $dataValues[$plotOrder[$i]][$j] = $dataValueX;
 561              }
 562          }
 563          if (empty($dataValues)) {
 564              return;
 565          }
 566  
 567          $dataValuesPlot = [];
 568          // Flatten the plot arrays to a single dimensional array to work with jpgraph
 569          $jMax = count($dataValues[0]);
 570          for ($j = 0; $j < $jMax; ++$j) {
 571              for ($i = 0; $i < $seriesCount; ++$i) {
 572                  $dataValuesPlot[] = $dataValues[$i][$j] ?? null;
 573              }
 574          }
 575  
 576          // Set the x-axis labels
 577          $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount();
 578          if ($labelCount > 0) {
 579              $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues();
 580              $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels);
 581              $this->graph->xaxis->SetTickLabels($datasetLabels);
 582          }
 583  
 584          $seriesPlot = new StockPlot($dataValuesPlot);
 585          $seriesPlot->SetWidth(20);
 586  
 587          $this->graph->Add($seriesPlot);
 588      }
 589  
 590      private function renderAreaChart($groupCount): void
 591      {
 592          $this->renderCartesianPlotArea();
 593  
 594          for ($i = 0; $i < $groupCount; ++$i) {
 595              $this->renderPlotLine($i, true, false);
 596          }
 597      }
 598  
 599      private function renderLineChart($groupCount): void
 600      {
 601          $this->renderCartesianPlotArea();
 602  
 603          for ($i = 0; $i < $groupCount; ++$i) {
 604              $this->renderPlotLine($i, false, false);
 605          }
 606      }
 607  
 608      private function renderBarChart($groupCount, $dimensions = '2d'): void
 609      {
 610          $this->renderCartesianPlotArea();
 611  
 612          for ($i = 0; $i < $groupCount; ++$i) {
 613              $this->renderPlotBar($i, $dimensions);
 614          }
 615      }
 616  
 617      private function renderScatterChart($groupCount): void
 618      {
 619          $this->renderCartesianPlotArea('linlin');
 620  
 621          for ($i = 0; $i < $groupCount; ++$i) {
 622              $this->renderPlotScatter($i, false);
 623          }
 624      }
 625  
 626      private function renderBubbleChart($groupCount): void
 627      {
 628          $this->renderCartesianPlotArea('linlin');
 629  
 630          for ($i = 0; $i < $groupCount; ++$i) {
 631              $this->renderPlotScatter($i, true);
 632          }
 633      }
 634  
 635      private function renderPieChart($groupCount, $dimensions = '2d', $doughnut = false, $multiplePlots = false): void
 636      {
 637          $this->renderPiePlotArea();
 638  
 639          $iLimit = ($multiplePlots) ? $groupCount : 1;
 640          for ($groupID = 0; $groupID < $iLimit; ++$groupID) {
 641              $exploded = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle();
 642              $datasetLabels = [];
 643              if ($groupID == 0) {
 644                  $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount();
 645                  if ($labelCount > 0) {
 646                      $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues();
 647                      $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels);
 648                  }
 649              }
 650  
 651              $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
 652              //    For pie charts, we only display the first series: doughnut charts generally display all series
 653              $jLimit = ($multiplePlots) ? $seriesCount : 1;
 654              //    Loop through each data series in turn
 655              for ($j = 0; $j < $jLimit; ++$j) {
 656                  $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues();
 657  
 658                  //    Fill in any missing values in the $dataValues array
 659                  $testCurrentIndex = 0;
 660                  foreach ($dataValues as $k => $dataValue) {
 661                      while ($k != $testCurrentIndex) {
 662                          $dataValues[$testCurrentIndex] = null;
 663                          ++$testCurrentIndex;
 664                      }
 665                      ++$testCurrentIndex;
 666                  }
 667  
 668                  if ($dimensions == '3d') {
 669                      $seriesPlot = new PiePlot3D($dataValues);
 670                  } else {
 671                      if ($doughnut) {
 672                          $seriesPlot = new PiePlotC($dataValues);
 673                      } else {
 674                          $seriesPlot = new PiePlot($dataValues);
 675                      }
 676                  }
 677  
 678                  if ($multiplePlots) {
 679                      $seriesPlot->SetSize(($jLimit - $j) / ($jLimit * 4));
 680                  }
 681  
 682                  if ($doughnut && method_exists($seriesPlot, 'SetMidColor')) {
 683                      $seriesPlot->SetMidColor('white');
 684                  }
 685  
 686                  $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]);
 687                  if (count($datasetLabels) > 0) {
 688                      $seriesPlot->SetLabels(array_fill(0, count($datasetLabels), ''));
 689                  }
 690                  if ($dimensions != '3d') {
 691                      $seriesPlot->SetGuideLines(false);
 692                  }
 693                  if ($j == 0) {
 694                      if ($exploded) {
 695                          $seriesPlot->ExplodeAll();
 696                      }
 697                      $seriesPlot->SetLegends($datasetLabels);
 698                  }
 699  
 700                  $this->graph->Add($seriesPlot);
 701              }
 702          }
 703      }
 704  
 705      private function renderRadarChart($groupCount): void
 706      {
 707          $this->renderRadarPlotArea();
 708  
 709          for ($groupID = 0; $groupID < $groupCount; ++$groupID) {
 710              $this->renderPlotRadar($groupID);
 711          }
 712      }
 713  
 714      private function renderStockChart($groupCount): void
 715      {
 716          $this->renderCartesianPlotArea('intint');
 717  
 718          for ($groupID = 0; $groupID < $groupCount; ++$groupID) {
 719              $this->renderPlotStock($groupID);
 720          }
 721      }
 722  
 723      private function renderContourChart($groupCount): void
 724      {
 725          $this->renderCartesianPlotArea('intint');
 726  
 727          for ($i = 0; $i < $groupCount; ++$i) {
 728              $this->renderPlotContour($i);
 729          }
 730      }
 731  
 732      private function renderCombinationChart($groupCount, $outputDestination)
 733      {
 734          $this->renderCartesianPlotArea();
 735  
 736          for ($i = 0; $i < $groupCount; ++$i) {
 737              $dimensions = null;
 738              $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType();
 739              switch ($chartType) {
 740                  case 'area3DChart':
 741                  case 'areaChart':
 742                      $this->renderPlotLine($i, true, true);
 743  
 744                      break;
 745                  case 'bar3DChart':
 746                      $dimensions = '3d';
 747                      // no break
 748                  case 'barChart':
 749                      $this->renderPlotBar($i, $dimensions);
 750  
 751                      break;
 752                  case 'line3DChart':
 753                  case 'lineChart':
 754                      $this->renderPlotLine($i, false, true);
 755  
 756                      break;
 757                  case 'scatterChart':
 758                      $this->renderPlotScatter($i, false);
 759  
 760                      break;
 761                  case 'bubbleChart':
 762                      $this->renderPlotScatter($i, true);
 763  
 764                      break;
 765                  default:
 766                      $this->graph = null;
 767  
 768                      return false;
 769              }
 770          }
 771  
 772          $this->renderLegend();
 773  
 774          $this->graph->Stroke($outputDestination);
 775  
 776          return true;
 777      }
 778  
 779      public function render($outputDestination)
 780      {
 781          self::$plotColour = 0;
 782  
 783          $groupCount = $this->chart->getPlotArea()->getPlotGroupCount();
 784  
 785          $dimensions = null;
 786          if ($groupCount == 1) {
 787              $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType();
 788          } else {
 789              $chartTypes = [];
 790              for ($i = 0; $i < $groupCount; ++$i) {
 791                  $chartTypes[] = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType();
 792              }
 793              $chartTypes = array_unique($chartTypes);
 794              if (count($chartTypes) == 1) {
 795                  $chartType = array_pop($chartTypes);
 796              } elseif (count($chartTypes) == 0) {
 797                  echo 'Chart is not yet implemented<br />';
 798  
 799                  return false;
 800              } else {
 801                  return $this->renderCombinationChart($groupCount, $outputDestination);
 802              }
 803          }
 804  
 805          switch ($chartType) {
 806              case 'area3DChart':
 807                  $dimensions = '3d';
 808                  // no break
 809              case 'areaChart':
 810                  $this->renderAreaChart($groupCount);
 811  
 812                  break;
 813              case 'bar3DChart':
 814                  $dimensions = '3d';
 815                  // no break
 816              case 'barChart':
 817                  $this->renderBarChart($groupCount, $dimensions);
 818  
 819                  break;
 820              case 'line3DChart':
 821                  $dimensions = '3d';
 822                  // no break
 823              case 'lineChart':
 824                  $this->renderLineChart($groupCount);
 825  
 826                  break;
 827              case 'pie3DChart':
 828                  $dimensions = '3d';
 829                  // no break
 830              case 'pieChart':
 831                  $this->renderPieChart($groupCount, $dimensions, false, false);
 832  
 833                  break;
 834              case 'doughnut3DChart':
 835                  $dimensions = '3d';
 836                  // no break
 837              case 'doughnutChart':
 838                  $this->renderPieChart($groupCount, $dimensions, true, true);
 839  
 840                  break;
 841              case 'scatterChart':
 842                  $this->renderScatterChart($groupCount);
 843  
 844                  break;
 845              case 'bubbleChart':
 846                  $this->renderBubbleChart($groupCount);
 847  
 848                  break;
 849              case 'radarChart':
 850                  $this->renderRadarChart($groupCount);
 851  
 852                  break;
 853              case 'surface3DChart':
 854              case 'surfaceChart':
 855                  $this->renderContourChart($groupCount);
 856  
 857                  break;
 858              case 'stockChart':
 859                  $this->renderStockChart($groupCount);
 860  
 861                  break;
 862              default:
 863                  echo $chartType . ' is not yet implemented<br />';
 864  
 865                  return false;
 866          }
 867          $this->renderLegend();
 868  
 869          $this->graph->Stroke($outputDestination);
 870  
 871          return true;
 872      }
 873  }