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 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  
   3  namespace PhpOffice\PhpSpreadsheet\Chart;
   4  
   5  use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
   6  use PhpOffice\PhpSpreadsheet\Calculation\Functions;
   7  use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
   8  use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
   9  
  10  class DataSeriesValues extends Properties
  11  {
  12      const DATASERIES_TYPE_STRING = 'String';
  13      const DATASERIES_TYPE_NUMBER = 'Number';
  14  
  15      private const DATA_TYPE_VALUES = [
  16          self::DATASERIES_TYPE_STRING,
  17          self::DATASERIES_TYPE_NUMBER,
  18      ];
  19  
  20      /**
  21       * Series Data Type.
  22       *
  23       * @var string
  24       */
  25      private $dataType;
  26  
  27      /**
  28       * Series Data Source.
  29       *
  30       * @var ?string
  31       */
  32      private $dataSource;
  33  
  34      /**
  35       * Format Code.
  36       *
  37       * @var string
  38       */
  39      private $formatCode;
  40  
  41      /**
  42       * Series Point Marker.
  43       *
  44       * @var string
  45       */
  46      private $pointMarker;
  47  
  48      /** @var ChartColor */
  49      private $markerFillColor;
  50  
  51      /** @var ChartColor */
  52      private $markerBorderColor;
  53  
  54      /**
  55       * Series Point Size.
  56       *
  57       * @var int
  58       */
  59      private $pointSize = 3;
  60  
  61      /**
  62       * Point Count (The number of datapoints in the dataseries).
  63       *
  64       * @var int
  65       */
  66      private $pointCount = 0;
  67  
  68      /**
  69       * Data Values.
  70       *
  71       * @var mixed[]
  72       */
  73      private $dataValues = [];
  74  
  75      /**
  76       * Fill color (can be array with colors if dataseries have custom colors).
  77       *
  78       * @var null|ChartColor|ChartColor[]
  79       */
  80      private $fillColor;
  81  
  82      /** @var bool */
  83      private $scatterLines = true;
  84  
  85      /** @var bool */
  86      private $bubble3D = false;
  87  
  88      /** @var ?Layout */
  89      private $labelLayout;
  90  
  91      /** @var TrendLine[] */
  92      private $trendLines = [];
  93  
  94      /**
  95       * Create a new DataSeriesValues object.
  96       *
  97       * @param string $dataType
  98       * @param string $dataSource
  99       * @param null|mixed $formatCode
 100       * @param int $pointCount
 101       * @param mixed $dataValues
 102       * @param null|mixed $marker
 103       * @param null|ChartColor|ChartColor[]|string|string[] $fillColor
 104       * @param string $pointSize
 105       */
 106      public function __construct($dataType = self::DATASERIES_TYPE_NUMBER, $dataSource = null, $formatCode = null, $pointCount = 0, $dataValues = [], $marker = null, $fillColor = null, $pointSize = '3')
 107      {
 108          parent::__construct();
 109          $this->markerFillColor = new ChartColor();
 110          $this->markerBorderColor = new ChartColor();
 111          $this->setDataType($dataType);
 112          $this->dataSource = $dataSource;
 113          $this->formatCode = $formatCode;
 114          $this->pointCount = $pointCount;
 115          $this->dataValues = $dataValues;
 116          $this->pointMarker = $marker;
 117          if ($fillColor !== null) {
 118              $this->setFillColor($fillColor);
 119          }
 120          if (is_numeric($pointSize)) {
 121              $this->pointSize = (int) $pointSize;
 122          }
 123      }
 124  
 125      /**
 126       * Get Series Data Type.
 127       *
 128       * @return string
 129       */
 130      public function getDataType()
 131      {
 132          return $this->dataType;
 133      }
 134  
 135      /**
 136       * Set Series Data Type.
 137       *
 138       * @param string $dataType Datatype of this data series
 139       *                                Typical values are:
 140       *                                    DataSeriesValues::DATASERIES_TYPE_STRING
 141       *                                        Normally used for axis point values
 142       *                                    DataSeriesValues::DATASERIES_TYPE_NUMBER
 143       *                                        Normally used for chart data values
 144       *
 145       * @return $this
 146       */
 147      public function setDataType($dataType)
 148      {
 149          if (!in_array($dataType, self::DATA_TYPE_VALUES)) {
 150              throw new Exception('Invalid datatype for chart data series values');
 151          }
 152          $this->dataType = $dataType;
 153  
 154          return $this;
 155      }
 156  
 157      /**
 158       * Get Series Data Source (formula).
 159       *
 160       * @return ?string
 161       */
 162      public function getDataSource()
 163      {
 164          return $this->dataSource;
 165      }
 166  
 167      /**
 168       * Set Series Data Source (formula).
 169       *
 170       * @param ?string $dataSource
 171       *
 172       * @return $this
 173       */
 174      public function setDataSource($dataSource)
 175      {
 176          $this->dataSource = $dataSource;
 177  
 178          return $this;
 179      }
 180  
 181      /**
 182       * Get Point Marker.
 183       *
 184       * @return string
 185       */
 186      public function getPointMarker()
 187      {
 188          return $this->pointMarker;
 189      }
 190  
 191      /**
 192       * Set Point Marker.
 193       *
 194       * @param string $marker
 195       *
 196       * @return $this
 197       */
 198      public function setPointMarker($marker)
 199      {
 200          $this->pointMarker = $marker;
 201  
 202          return $this;
 203      }
 204  
 205      public function getMarkerFillColor(): ChartColor
 206      {
 207          return $this->markerFillColor;
 208      }
 209  
 210      public function getMarkerBorderColor(): ChartColor
 211      {
 212          return $this->markerBorderColor;
 213      }
 214  
 215      /**
 216       * Get Point Size.
 217       */
 218      public function getPointSize(): int
 219      {
 220          return $this->pointSize;
 221      }
 222  
 223      /**
 224       * Set Point Size.
 225       *
 226       * @return $this
 227       */
 228      public function setPointSize(int $size = 3)
 229      {
 230          $this->pointSize = $size;
 231  
 232          return $this;
 233      }
 234  
 235      /**
 236       * Get Series Format Code.
 237       *
 238       * @return string
 239       */
 240      public function getFormatCode()
 241      {
 242          return $this->formatCode;
 243      }
 244  
 245      /**
 246       * Set Series Format Code.
 247       *
 248       * @param string $formatCode
 249       *
 250       * @return $this
 251       */
 252      public function setFormatCode($formatCode)
 253      {
 254          $this->formatCode = $formatCode;
 255  
 256          return $this;
 257      }
 258  
 259      /**
 260       * Get Series Point Count.
 261       *
 262       * @return int
 263       */
 264      public function getPointCount()
 265      {
 266          return $this->pointCount;
 267      }
 268  
 269      /**
 270       * Get fill color object.
 271       *
 272       * @return null|ChartColor|ChartColor[]
 273       */
 274      public function getFillColorObject()
 275      {
 276          return $this->fillColor;
 277      }
 278  
 279      private function stringToChartColor(string $fillString): ChartColor
 280      {
 281          $value = $type = '';
 282          if (substr($fillString, 0, 1) === '*') {
 283              $type = 'schemeClr';
 284              $value = substr($fillString, 1);
 285          } elseif (substr($fillString, 0, 1) === '/') {
 286              $type = 'prstClr';
 287              $value = substr($fillString, 1);
 288          } elseif ($fillString !== '') {
 289              $type = 'srgbClr';
 290              $value = $fillString;
 291              $this->validateColor($value);
 292          }
 293  
 294          return new ChartColor($value, null, $type);
 295      }
 296  
 297      private function chartColorToString(ChartColor $chartColor): string
 298      {
 299          $type = (string) $chartColor->getColorProperty('type');
 300          $value = (string) $chartColor->getColorProperty('value');
 301          if ($type === '' || $value === '') {
 302              return '';
 303          }
 304          if ($type === 'schemeClr') {
 305              return "*$value";
 306          }
 307          if ($type === 'prstClr') {
 308              return "/$value";
 309          }
 310  
 311          return $value;
 312      }
 313  
 314      /**
 315       * Get fill color.
 316       *
 317       * @return string|string[] HEX color or array with HEX colors
 318       */
 319      public function getFillColor()
 320      {
 321          if ($this->fillColor === null) {
 322              return '';
 323          }
 324          if (is_array($this->fillColor)) {
 325              $array = [];
 326              foreach ($this->fillColor as $chartColor) {
 327                  $array[] = $this->chartColorToString($chartColor);
 328              }
 329  
 330              return $array;
 331          }
 332  
 333          return $this->chartColorToString($this->fillColor);
 334      }
 335  
 336      /**
 337       * Set fill color for series.
 338       *
 339       * @param ChartColor|ChartColor[]|string|string[] $color HEX color or array with HEX colors
 340       *
 341       * @return   DataSeriesValues
 342       */
 343      public function setFillColor($color)
 344      {
 345          if (is_array($color)) {
 346              $this->fillColor = [];
 347              foreach ($color as $fillString) {
 348                  if ($fillString instanceof ChartColor) {
 349                      $this->fillColor[] = $fillString;
 350                  } else {
 351                      $this->fillColor[] = $this->stringToChartColor($fillString);
 352                  }
 353              }
 354          } elseif ($color instanceof ChartColor) {
 355              $this->fillColor = $color;
 356          } else {
 357              $this->fillColor = $this->stringToChartColor($color);
 358          }
 359  
 360          return $this;
 361      }
 362  
 363      /**
 364       * Method for validating hex color.
 365       *
 366       * @param string $color value for color
 367       *
 368       * @return bool true if validation was successful
 369       */
 370      private function validateColor($color)
 371      {
 372          if (!preg_match('/^[a-f0-9]{6}$/i', $color)) {
 373              throw new Exception(sprintf('Invalid hex color for chart series (color: "%s")', $color));
 374          }
 375  
 376          return true;
 377      }
 378  
 379      /**
 380       * Get line width for series.
 381       *
 382       * @return null|float|int
 383       */
 384      public function getLineWidth()
 385      {
 386          return $this->lineStyleProperties['width'];
 387      }
 388  
 389      /**
 390       * Set line width for the series.
 391       *
 392       * @param null|float|int $width
 393       *
 394       * @return $this
 395       */
 396      public function setLineWidth($width)
 397      {
 398          $this->lineStyleProperties['width'] = $width;
 399  
 400          return $this;
 401      }
 402  
 403      /**
 404       * Identify if the Data Series is a multi-level or a simple series.
 405       *
 406       * @return null|bool
 407       */
 408      public function isMultiLevelSeries()
 409      {
 410          if (!empty($this->dataValues)) {
 411              return is_array(array_values($this->dataValues)[0]);
 412          }
 413  
 414          return null;
 415      }
 416  
 417      /**
 418       * Return the level count of a multi-level Data Series.
 419       *
 420       * @return int
 421       */
 422      public function multiLevelCount()
 423      {
 424          $levelCount = 0;
 425          foreach ($this->dataValues as $dataValueSet) {
 426              $levelCount = max($levelCount, count($dataValueSet));
 427          }
 428  
 429          return $levelCount;
 430      }
 431  
 432      /**
 433       * Get Series Data Values.
 434       *
 435       * @return mixed[]
 436       */
 437      public function getDataValues()
 438      {
 439          return $this->dataValues;
 440      }
 441  
 442      /**
 443       * Get the first Series Data value.
 444       *
 445       * @return mixed
 446       */
 447      public function getDataValue()
 448      {
 449          $count = count($this->dataValues);
 450          if ($count == 0) {
 451              return null;
 452          } elseif ($count == 1) {
 453              return $this->dataValues[0];
 454          }
 455  
 456          return $this->dataValues;
 457      }
 458  
 459      /**
 460       * Set Series Data Values.
 461       *
 462       * @param array $dataValues
 463       *
 464       * @return $this
 465       */
 466      public function setDataValues($dataValues)
 467      {
 468          $this->dataValues = Functions::flattenArray($dataValues);
 469          $this->pointCount = count($dataValues);
 470  
 471          return $this;
 472      }
 473  
 474      public function refresh(Worksheet $worksheet, bool $flatten = true): void
 475      {
 476          if ($this->dataSource !== null) {
 477              $calcEngine = Calculation::getInstance($worksheet->getParent());
 478              $newDataValues = Calculation::unwrapResult(
 479                  $calcEngine->_calculateFormulaValue(
 480                      '=' . $this->dataSource,
 481                      null,
 482                      $worksheet->getCell('A1')
 483                  )
 484              );
 485              if ($flatten) {
 486                  $this->dataValues = Functions::flattenArray($newDataValues);
 487                  foreach ($this->dataValues as &$dataValue) {
 488                      if (is_string($dataValue) && !empty($dataValue) && $dataValue[0] == '#') {
 489                          $dataValue = 0.0;
 490                      }
 491                  }
 492                  unset($dataValue);
 493              } else {
 494                  [$worksheet, $cellRange] = Worksheet::extractSheetTitle($this->dataSource, true);
 495                  $dimensions = Coordinate::rangeDimension(str_replace('$', '', $cellRange));
 496                  if (($dimensions[0] == 1) || ($dimensions[1] == 1)) {
 497                      $this->dataValues = Functions::flattenArray($newDataValues);
 498                  } else {
 499                      $newArray = array_values(array_shift($newDataValues));
 500                      foreach ($newArray as $i => $newDataSet) {
 501                          $newArray[$i] = [$newDataSet];
 502                      }
 503  
 504                      foreach ($newDataValues as $newDataSet) {
 505                          $i = 0;
 506                          foreach ($newDataSet as $newDataVal) {
 507                              array_unshift($newArray[$i++], $newDataVal);
 508                          }
 509                      }
 510                      $this->dataValues = $newArray;
 511                  }
 512              }
 513              $this->pointCount = count($this->dataValues);
 514          }
 515      }
 516  
 517      public function getScatterLines(): bool
 518      {
 519          return $this->scatterLines;
 520      }
 521  
 522      public function setScatterLines(bool $scatterLines): self
 523      {
 524          $this->scatterLines = $scatterLines;
 525  
 526          return $this;
 527      }
 528  
 529      public function getBubble3D(): bool
 530      {
 531          return $this->bubble3D;
 532      }
 533  
 534      public function setBubble3D(bool $bubble3D): self
 535      {
 536          $this->bubble3D = $bubble3D;
 537  
 538          return $this;
 539      }
 540  
 541      /**
 542       * Smooth Line. Must be specified for both DataSeries and DataSeriesValues.
 543       *
 544       * @var bool
 545       */
 546      private $smoothLine;
 547  
 548      /**
 549       * Get Smooth Line.
 550       *
 551       * @return bool
 552       */
 553      public function getSmoothLine()
 554      {
 555          return $this->smoothLine;
 556      }
 557  
 558      /**
 559       * Set Smooth Line.
 560       *
 561       * @param bool $smoothLine
 562       *
 563       * @return $this
 564       */
 565      public function setSmoothLine($smoothLine)
 566      {
 567          $this->smoothLine = $smoothLine;
 568  
 569          return $this;
 570      }
 571  
 572      public function getLabelLayout(): ?Layout
 573      {
 574          return $this->labelLayout;
 575      }
 576  
 577      public function setLabelLayout(?Layout $labelLayout): self
 578      {
 579          $this->labelLayout = $labelLayout;
 580  
 581          return $this;
 582      }
 583  
 584      public function setTrendLines(array $trendLines): self
 585      {
 586          $this->trendLines = $trendLines;
 587  
 588          return $this;
 589      }
 590  
 591      public function getTrendLines(): array
 592      {
 593          return $this->trendLines;
 594      }
 595  }