Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

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