Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 401 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              $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues();
 438              $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
 439  
 440              foreach ($dataValuesY as $k => $dataValueY) {
 441                  $dataValuesY[$k] = $k;
 442              }
 443  
 444              $seriesPlot = new ScatterPlot($dataValuesX, $dataValuesY);
 445              if ($scatterStyle == 'lineMarker') {
 446                  $seriesPlot->SetLinkPoints();
 447                  $seriesPlot->link->SetColor(self::$colourSet[self::$plotColour]);
 448              } elseif ($scatterStyle == 'smoothMarker') {
 449                  $spline = new Spline($dataValuesY, $dataValuesX);
 450                  [$splineDataY, $splineDataX] = $spline->Get(count($dataValuesX) * self::$width / 20);
 451                  $lplot = new LinePlot($splineDataX, $splineDataY);
 452                  $lplot->SetColor(self::$colourSet[self::$plotColour]);
 453  
 454                  $this->graph->Add($lplot);
 455              }
 456  
 457              if ($bubble) {
 458                  $this->formatPointMarker($seriesPlot, 'dot');
 459                  $seriesPlot->mark->SetColor('black');
 460                  $seriesPlot->mark->SetSize($bubbleSize);
 461              } else {
 462                  $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker();
 463                  $this->formatPointMarker($seriesPlot, $marker);
 464              }
 465              $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue();
 466              $seriesPlot->SetLegend($dataLabel);
 467  
 468              $this->graph->Add($seriesPlot);
 469          }
 470      }
 471  
 472      private function renderPlotRadar($groupID): void
 473      {
 474          $radarStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle();
 475  
 476          $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
 477  
 478          //    Loop through each data series in turn
 479          for ($i = 0; $i < $seriesCount; ++$i) {
 480              $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues();
 481              $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
 482              $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker();
 483  
 484              $dataValues = [];
 485              foreach ($dataValuesY as $k => $dataValueY) {
 486                  $dataValues[$k] = implode(' ', array_reverse($dataValueY));
 487              }
 488              $tmp = array_shift($dataValues);
 489              $dataValues[] = $tmp;
 490              $tmp = array_shift($dataValuesX);
 491              $dataValuesX[] = $tmp;
 492  
 493              $this->graph->SetTitles(array_reverse($dataValues));
 494  
 495              $seriesPlot = new RadarPlot(array_reverse($dataValuesX));
 496  
 497              $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue();
 498              $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]);
 499              if ($radarStyle == 'filled') {
 500                  $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour]);
 501              }
 502              $this->formatPointMarker($seriesPlot, $marker);
 503              $seriesPlot->SetLegend($dataLabel);
 504  
 505              $this->graph->Add($seriesPlot);
 506          }
 507      }
 508  
 509      private function renderPlotContour($groupID): void
 510      {
 511          $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
 512  
 513          $dataValues = [];
 514          //    Loop through each data series in turn
 515          for ($i = 0; $i < $seriesCount; ++$i) {
 516              $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
 517  
 518              $dataValues[$i] = $dataValuesX;
 519          }
 520          $seriesPlot = new ContourPlot($dataValues);
 521  
 522          $this->graph->Add($seriesPlot);
 523      }
 524  
 525      private function renderPlotStock($groupID): void
 526      {
 527          $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
 528          $plotOrder = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder();
 529  
 530          $dataValues = [];
 531          //    Loop through each data series in turn and build the plot arrays
 532          foreach ($plotOrder as $i => $v) {
 533              $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v);
 534              if ($dataValuesX === false) {
 535                  continue;
 536              }
 537              $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v)->getDataValues();
 538              foreach ($dataValuesX as $j => $dataValueX) {
 539                  $dataValues[$plotOrder[$i]][$j] = $dataValueX;
 540              }
 541          }
 542          if (empty($dataValues)) {
 543              return;
 544          }
 545  
 546          $dataValuesPlot = [];
 547          // Flatten the plot arrays to a single dimensional array to work with jpgraph
 548          $jMax = count($dataValues[0]);
 549          for ($j = 0; $j < $jMax; ++$j) {
 550              for ($i = 0; $i < $seriesCount; ++$i) {
 551                  $dataValuesPlot[] = $dataValues[$i][$j] ?? null;
 552              }
 553          }
 554  
 555          // Set the x-axis labels
 556          $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount();
 557          if ($labelCount > 0) {
 558              $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues();
 559              $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels);
 560              $this->graph->xaxis->SetTickLabels($datasetLabels);
 561          }
 562  
 563          $seriesPlot = new StockPlot($dataValuesPlot);
 564          $seriesPlot->SetWidth(20);
 565  
 566          $this->graph->Add($seriesPlot);
 567      }
 568  
 569      private function renderAreaChart($groupCount): void
 570      {
 571          $this->renderCartesianPlotArea();
 572  
 573          for ($i = 0; $i < $groupCount; ++$i) {
 574              $this->renderPlotLine($i, true, false);
 575          }
 576      }
 577  
 578      private function renderLineChart($groupCount): void
 579      {
 580          $this->renderCartesianPlotArea();
 581  
 582          for ($i = 0; $i < $groupCount; ++$i) {
 583              $this->renderPlotLine($i, false, false);
 584          }
 585      }
 586  
 587      private function renderBarChart($groupCount, $dimensions = '2d'): void
 588      {
 589          $this->renderCartesianPlotArea();
 590  
 591          for ($i = 0; $i < $groupCount; ++$i) {
 592              $this->renderPlotBar($i, $dimensions);
 593          }
 594      }
 595  
 596      private function renderScatterChart($groupCount): void
 597      {
 598          $this->renderCartesianPlotArea('linlin');
 599  
 600          for ($i = 0; $i < $groupCount; ++$i) {
 601              $this->renderPlotScatter($i, false);
 602          }
 603      }
 604  
 605      private function renderBubbleChart($groupCount): void
 606      {
 607          $this->renderCartesianPlotArea('linlin');
 608  
 609          for ($i = 0; $i < $groupCount; ++$i) {
 610              $this->renderPlotScatter($i, true);
 611          }
 612      }
 613  
 614      private function renderPieChart($groupCount, $dimensions = '2d', $doughnut = false, $multiplePlots = false): void
 615      {
 616          $this->renderPiePlotArea();
 617  
 618          $iLimit = ($multiplePlots) ? $groupCount : 1;
 619          for ($groupID = 0; $groupID < $iLimit; ++$groupID) {
 620              $exploded = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle();
 621              $datasetLabels = [];
 622              if ($groupID == 0) {
 623                  $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount();
 624                  if ($labelCount > 0) {
 625                      $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues();
 626                      $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels);
 627                  }
 628              }
 629  
 630              $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
 631              //    For pie charts, we only display the first series: doughnut charts generally display all series
 632              $jLimit = ($multiplePlots) ? $seriesCount : 1;
 633              //    Loop through each data series in turn
 634              for ($j = 0; $j < $jLimit; ++$j) {
 635                  $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues();
 636  
 637                  //    Fill in any missing values in the $dataValues array
 638                  $testCurrentIndex = 0;
 639                  foreach ($dataValues as $k => $dataValue) {
 640                      while ($k != $testCurrentIndex) {
 641                          $dataValues[$testCurrentIndex] = null;
 642                          ++$testCurrentIndex;
 643                      }
 644                      ++$testCurrentIndex;
 645                  }
 646  
 647                  if ($dimensions == '3d') {
 648                      $seriesPlot = new PiePlot3D($dataValues);
 649                  } else {
 650                      if ($doughnut) {
 651                          $seriesPlot = new PiePlotC($dataValues);
 652                      } else {
 653                          $seriesPlot = new PiePlot($dataValues);
 654                      }
 655                  }
 656  
 657                  if ($multiplePlots) {
 658                      $seriesPlot->SetSize(($jLimit - $j) / ($jLimit * 4));
 659                  }
 660  
 661                  if ($doughnut && method_exists($seriesPlot, 'SetMidColor')) {
 662                      $seriesPlot->SetMidColor('white');
 663                  }
 664  
 665                  $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]);
 666                  if (count($datasetLabels) > 0) {
 667                      $seriesPlot->SetLabels(array_fill(0, count($datasetLabels), ''));
 668                  }
 669                  if ($dimensions != '3d') {
 670                      $seriesPlot->SetGuideLines(false);
 671                  }
 672                  if ($j == 0) {
 673                      if ($exploded) {
 674                          $seriesPlot->ExplodeAll();
 675                      }
 676                      $seriesPlot->SetLegends($datasetLabels);
 677                  }
 678  
 679                  $this->graph->Add($seriesPlot);
 680              }
 681          }
 682      }
 683  
 684      private function renderRadarChart($groupCount): void
 685      {
 686          $this->renderRadarPlotArea();
 687  
 688          for ($groupID = 0; $groupID < $groupCount; ++$groupID) {
 689              $this->renderPlotRadar($groupID);
 690          }
 691      }
 692  
 693      private function renderStockChart($groupCount): void
 694      {
 695          $this->renderCartesianPlotArea('intint');
 696  
 697          for ($groupID = 0; $groupID < $groupCount; ++$groupID) {
 698              $this->renderPlotStock($groupID);
 699          }
 700      }
 701  
 702      private function renderContourChart($groupCount): void
 703      {
 704          $this->renderCartesianPlotArea('intint');
 705  
 706          for ($i = 0; $i < $groupCount; ++$i) {
 707              $this->renderPlotContour($i);
 708          }
 709      }
 710  
 711      private function renderCombinationChart($groupCount, $outputDestination)
 712      {
 713          $this->renderCartesianPlotArea();
 714  
 715          for ($i = 0; $i < $groupCount; ++$i) {
 716              $dimensions = null;
 717              $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType();
 718              switch ($chartType) {
 719                  case 'area3DChart':
 720                  case 'areaChart':
 721                      $this->renderPlotLine($i, true, true);
 722  
 723                      break;
 724                  case 'bar3DChart':
 725                      $dimensions = '3d';
 726                      // no break
 727                  case 'barChart':
 728                      $this->renderPlotBar($i, $dimensions);
 729  
 730                      break;
 731                  case 'line3DChart':
 732                  case 'lineChart':
 733                      $this->renderPlotLine($i, false, true);
 734  
 735                      break;
 736                  case 'scatterChart':
 737                      $this->renderPlotScatter($i, false);
 738  
 739                      break;
 740                  case 'bubbleChart':
 741                      $this->renderPlotScatter($i, true);
 742  
 743                      break;
 744                  default:
 745                      $this->graph = null;
 746  
 747                      return false;
 748              }
 749          }
 750  
 751          $this->renderLegend();
 752  
 753          $this->graph->Stroke($outputDestination);
 754  
 755          return true;
 756      }
 757  
 758      public function render($outputDestination)
 759      {
 760          self::$plotColour = 0;
 761  
 762          $groupCount = $this->chart->getPlotArea()->getPlotGroupCount();
 763  
 764          $dimensions = null;
 765          if ($groupCount == 1) {
 766              $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType();
 767          } else {
 768              $chartTypes = [];
 769              for ($i = 0; $i < $groupCount; ++$i) {
 770                  $chartTypes[] = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType();
 771              }
 772              $chartTypes = array_unique($chartTypes);
 773              if (count($chartTypes) == 1) {
 774                  $chartType = array_pop($chartTypes);
 775              } elseif (count($chartTypes) == 0) {
 776                  echo 'Chart is not yet implemented<br />';
 777  
 778                  return false;
 779              } else {
 780                  return $this->renderCombinationChart($groupCount, $outputDestination);
 781              }
 782          }
 783  
 784          switch ($chartType) {
 785              case 'area3DChart':
 786                  $dimensions = '3d';
 787                  // no break
 788              case 'areaChart':
 789                  $this->renderAreaChart($groupCount);
 790  
 791                  break;
 792              case 'bar3DChart':
 793                  $dimensions = '3d';
 794                  // no break
 795              case 'barChart':
 796                  $this->renderBarChart($groupCount, $dimensions);
 797  
 798                  break;
 799              case 'line3DChart':
 800                  $dimensions = '3d';
 801                  // no break
 802              case 'lineChart':
 803                  $this->renderLineChart($groupCount);
 804  
 805                  break;
 806              case 'pie3DChart':
 807                  $dimensions = '3d';
 808                  // no break
 809              case 'pieChart':
 810                  $this->renderPieChart($groupCount, $dimensions, false, false);
 811  
 812                  break;
 813              case 'doughnut3DChart':
 814                  $dimensions = '3d';
 815                  // no break
 816              case 'doughnutChart':
 817                  $this->renderPieChart($groupCount, $dimensions, true, true);
 818  
 819                  break;
 820              case 'scatterChart':
 821                  $this->renderScatterChart($groupCount);
 822  
 823                  break;
 824              case 'bubbleChart':
 825                  $this->renderBubbleChart($groupCount);
 826  
 827                  break;
 828              case 'radarChart':
 829                  $this->renderRadarChart($groupCount);
 830  
 831                  break;
 832              case 'surface3DChart':
 833              case 'surfaceChart':
 834                  $this->renderContourChart($groupCount);
 835  
 836                  break;
 837              case 'stockChart':
 838                  $this->renderStockChart($groupCount);
 839  
 840                  break;
 841              default:
 842                  echo $chartType . ' is not yet implemented<br />';
 843  
 844                  return false;
 845          }
 846          $this->renderLegend();
 847  
 848          $this->graph->Stroke($outputDestination);
 849  
 850          return true;
 851      }
 852  }