Search moodle.org's
Developer Documentation

See Release Notes

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

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

   1  <?php
   2  
   3  namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
   4  
   5  use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
   6  use PhpOffice\PhpSpreadsheet\Chart\Axis;
   7  use PhpOffice\PhpSpreadsheet\Chart\AxisText;
   8  use PhpOffice\PhpSpreadsheet\Chart\ChartColor;
   9  use PhpOffice\PhpSpreadsheet\Chart\DataSeries;
  10  use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues;
  11  use PhpOffice\PhpSpreadsheet\Chart\GridLines;
  12  use PhpOffice\PhpSpreadsheet\Chart\Layout;
  13  use PhpOffice\PhpSpreadsheet\Chart\Legend;
  14  use PhpOffice\PhpSpreadsheet\Chart\PlotArea;
  15  use PhpOffice\PhpSpreadsheet\Chart\Properties as ChartProperties;
  16  use PhpOffice\PhpSpreadsheet\Chart\Title;
  17  use PhpOffice\PhpSpreadsheet\Chart\TrendLine;
  18  use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
  19  use PhpOffice\PhpSpreadsheet\RichText\RichText;
  20  use PhpOffice\PhpSpreadsheet\Style\Font;
  21  use SimpleXMLElement;
  22  
  23  class Chart
  24  {
  25      /** @var string */
  26      private $cNamespace;
  27  
  28      /** @var string */
  29      private $aNamespace;
  30  
  31      public function __construct(string $cNamespace = Namespaces::CHART, string $aNamespace = Namespaces::DRAWINGML)
  32      {
  33          $this->cNamespace = $cNamespace;
  34          $this->aNamespace = $aNamespace;
  35      }
  36  
  37      /**
  38       * @param string $name
  39       * @param string $format
  40       *
  41       * @return null|bool|float|int|string
  42       */
  43      private static function getAttribute(SimpleXMLElement $component, $name, $format)
  44      {
  45          $attributes = $component->attributes();
  46          if (@isset($attributes[$name])) {
  47              if ($format == 'string') {
  48                  return (string) $attributes[$name];
  49              } elseif ($format == 'integer') {
  50                  return (int) $attributes[$name];
  51              } elseif ($format == 'boolean') {
  52                  $value = (string) $attributes[$name];
  53  
  54                  return $value === 'true' || $value === '1';
  55              }
  56  
  57              return (float) $attributes[$name];
  58          }
  59  
  60          return null;
  61      }
  62  
  63      /**
  64       * @param string $chartName
  65       *
  66       * @return \PhpOffice\PhpSpreadsheet\Chart\Chart
  67       */
  68      public function readChart(SimpleXMLElement $chartElements, $chartName)
  69      {
  70          $chartElementsC = $chartElements->children($this->cNamespace);
  71  
  72          $XaxisLabel = $YaxisLabel = $legend = $title = null;
  73          $dispBlanksAs = $plotVisOnly = null;
  74          $plotArea = null;
  75          $rotX = $rotY = $rAngAx = $perspective = null;
  76          $xAxis = new Axis();
  77          $yAxis = new Axis();
  78          $autoTitleDeleted = null;
  79          $chartNoFill = false;
  80          $chartBorderLines = null;
  81          $chartFillColor = null;
  82          $gradientArray = [];
  83          $gradientLin = null;
  84          $roundedCorners = false;
  85          $gapWidth = null;
  86          $useUpBars = null;
  87          $useDownBars = null;
  88          foreach ($chartElementsC as $chartElementKey => $chartElement) {
  89              switch ($chartElementKey) {
  90                  case 'spPr':
  91                      $children = $chartElementsC->spPr->children($this->aNamespace);
  92                      if (isset($children->noFill)) {
  93                          $chartNoFill = true;
  94                      }
  95                      if (isset($children->solidFill)) {
  96                          $chartFillColor = $this->readColor($children->solidFill);
  97                      }
  98                      if (isset($children->ln)) {
  99                          $chartBorderLines = new GridLines();
 100                          $this->readLineStyle($chartElementsC, $chartBorderLines);
 101                      }
 102  
 103                      break;
 104                  case 'roundedCorners':
 105                      /** @var bool */
 106                      $roundedCorners = self::getAttribute($chartElementsC->roundedCorners, 'val', 'boolean');
 107  
 108                      break;
 109                  case 'chart':
 110                      foreach ($chartElement as $chartDetailsKey => $chartDetails) {
 111                          $chartDetails = Xlsx::testSimpleXml($chartDetails);
 112                          switch ($chartDetailsKey) {
 113                              case 'autoTitleDeleted':
 114                                  /** @var bool */
 115                                  $autoTitleDeleted = self::getAttribute($chartElementsC->chart->autoTitleDeleted, 'val', 'boolean');
 116  
 117                                  break;
 118                              case 'view3D':
 119                                  $rotX = self::getAttribute($chartDetails->rotX, 'val', 'integer');
 120                                  $rotY = self::getAttribute($chartDetails->rotY, 'val', 'integer');
 121                                  $rAngAx = self::getAttribute($chartDetails->rAngAx, 'val', 'integer');
 122                                  $perspective = self::getAttribute($chartDetails->perspective, 'val', 'integer');
 123  
 124                                  break;
 125                              case 'plotArea':
 126                                  $plotAreaLayout = $XaxisLabel = $YaxisLabel = null;
 127                                  $plotSeries = $plotAttributes = [];
 128                                  $catAxRead = false;
 129                                  $plotNoFill = false;
 130                                  foreach ($chartDetails as $chartDetailKey => $chartDetail) {
 131                                      $chartDetail = Xlsx::testSimpleXml($chartDetail);
 132                                      switch ($chartDetailKey) {
 133                                          case 'spPr':
 134                                              $possibleNoFill = $chartDetails->spPr->children($this->aNamespace);
 135                                              if (isset($possibleNoFill->noFill)) {
 136                                                  $plotNoFill = true;
 137                                              }
 138                                              if (isset($possibleNoFill->gradFill->gsLst)) {
 139                                                  foreach ($possibleNoFill->gradFill->gsLst->gs as $gradient) {
 140                                                      $gradient = Xlsx::testSimpleXml($gradient);
 141                                                      /** @var float */
 142                                                      $pos = self::getAttribute($gradient, 'pos', 'float');
 143                                                      $gradientArray[] = [
 144                                                          $pos / ChartProperties::PERCENTAGE_MULTIPLIER,
 145                                                          new ChartColor($this->readColor($gradient)),
 146                                                      ];
 147                                                  }
 148                                              }
 149                                              if (isset($possibleNoFill->gradFill->lin)) {
 150                                                  $gradientLin = ChartProperties::XmlToAngle((string) self::getAttribute($possibleNoFill->gradFill->lin, 'ang', 'string'));
 151                                              }
 152  
 153                                              break;
 154                                          case 'layout':
 155                                              $plotAreaLayout = $this->chartLayoutDetails($chartDetail);
 156  
 157                                              break;
 158                                          case Axis::AXIS_TYPE_CATEGORY:
 159                                          case Axis::AXIS_TYPE_DATE:
 160                                              $catAxRead = true;
 161                                              if (isset($chartDetail->title)) {
 162                                                  $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace));
 163                                              }
 164                                              $xAxis->setAxisType($chartDetailKey);
 165                                              $this->readEffects($chartDetail, $xAxis);
 166                                              $this->readLineStyle($chartDetail, $xAxis);
 167                                              if (isset($chartDetail->spPr)) {
 168                                                  $sppr = $chartDetail->spPr->children($this->aNamespace);
 169                                                  if (isset($sppr->solidFill)) {
 170                                                      $axisColorArray = $this->readColor($sppr->solidFill);
 171                                                      $xAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']);
 172                                                  }
 173                                                  if (isset($chartDetail->spPr->ln->noFill)) {
 174                                                      $xAxis->setNoFill(true);
 175                                                  }
 176                                              }
 177                                              if (isset($chartDetail->majorGridlines)) {
 178                                                  $majorGridlines = new GridLines();
 179                                                  if (isset($chartDetail->majorGridlines->spPr)) {
 180                                                      $this->readEffects($chartDetail->majorGridlines, $majorGridlines);
 181                                                      $this->readLineStyle($chartDetail->majorGridlines, $majorGridlines);
 182                                                  }
 183                                                  $xAxis->setMajorGridlines($majorGridlines);
 184                                              }
 185                                              if (isset($chartDetail->minorGridlines)) {
 186                                                  $minorGridlines = new GridLines();
 187                                                  $minorGridlines->activateObject();
 188                                                  if (isset($chartDetail->minorGridlines->spPr)) {
 189                                                      $this->readEffects($chartDetail->minorGridlines, $minorGridlines);
 190                                                      $this->readLineStyle($chartDetail->minorGridlines, $minorGridlines);
 191                                                  }
 192                                                  $xAxis->setMinorGridlines($minorGridlines);
 193                                              }
 194                                              $this->setAxisProperties($chartDetail, $xAxis);
 195  
 196                                              break;
 197                                          case Axis::AXIS_TYPE_VALUE:
 198                                              $whichAxis = null;
 199                                              $axPos = null;
 200                                              if (isset($chartDetail->axPos)) {
 201                                                  $axPos = self::getAttribute($chartDetail->axPos, 'val', 'string');
 202                                              }
 203                                              if ($catAxRead) {
 204                                                  $whichAxis = $yAxis;
 205                                                  $yAxis->setAxisType($chartDetailKey);
 206                                              } elseif (!empty($axPos)) {
 207                                                  switch ($axPos) {
 208                                                      case 't':
 209                                                      case 'b':
 210                                                          $whichAxis = $xAxis;
 211                                                          $xAxis->setAxisType($chartDetailKey);
 212  
 213                                                          break;
 214                                                      case 'r':
 215                                                      case 'l':
 216                                                          $whichAxis = $yAxis;
 217                                                          $yAxis->setAxisType($chartDetailKey);
 218  
 219                                                          break;
 220                                                  }
 221                                              }
 222                                              if (isset($chartDetail->title)) {
 223                                                  $axisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace));
 224  
 225                                                  switch ($axPos) {
 226                                                      case 't':
 227                                                      case 'b':
 228                                                          $XaxisLabel = $axisLabel;
 229  
 230                                                          break;
 231                                                      case 'r':
 232                                                      case 'l':
 233                                                          $YaxisLabel = $axisLabel;
 234  
 235                                                          break;
 236                                                  }
 237                                              }
 238                                              $this->readEffects($chartDetail, $whichAxis);
 239                                              $this->readLineStyle($chartDetail, $whichAxis);
 240                                              if ($whichAxis !== null && isset($chartDetail->spPr)) {
 241                                                  $sppr = $chartDetail->spPr->children($this->aNamespace);
 242                                                  if (isset($sppr->solidFill)) {
 243                                                      $axisColorArray = $this->readColor($sppr->solidFill);
 244                                                      $whichAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']);
 245                                                  }
 246                                                  if (isset($sppr->ln->noFill)) {
 247                                                      $whichAxis->setNoFill(true);
 248                                                  }
 249                                              }
 250                                              if ($whichAxis !== null && isset($chartDetail->majorGridlines)) {
 251                                                  $majorGridlines = new GridLines();
 252                                                  if (isset($chartDetail->majorGridlines->spPr)) {
 253                                                      $this->readEffects($chartDetail->majorGridlines, $majorGridlines);
 254                                                      $this->readLineStyle($chartDetail->majorGridlines, $majorGridlines);
 255                                                  }
 256                                                  $whichAxis->setMajorGridlines($majorGridlines);
 257                                              }
 258                                              if ($whichAxis !== null && isset($chartDetail->minorGridlines)) {
 259                                                  $minorGridlines = new GridLines();
 260                                                  $minorGridlines->activateObject();
 261                                                  if (isset($chartDetail->minorGridlines->spPr)) {
 262                                                      $this->readEffects($chartDetail->minorGridlines, $minorGridlines);
 263                                                      $this->readLineStyle($chartDetail->minorGridlines, $minorGridlines);
 264                                                  }
 265                                                  $whichAxis->setMinorGridlines($minorGridlines);
 266                                              }
 267                                              $this->setAxisProperties($chartDetail, $whichAxis);
 268  
 269                                              break;
 270                                          case 'barChart':
 271                                          case 'bar3DChart':
 272                                              $barDirection = self::getAttribute($chartDetail->barDir, 'val', 'string');
 273                                              $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey);
 274                                              $plotSer->setPlotDirection("$barDirection");
 275                                              $plotSeries[] = $plotSer;
 276                                              $plotAttributes = $this->readChartAttributes($chartDetail);
 277  
 278                                              break;
 279                                          case 'lineChart':
 280                                          case 'line3DChart':
 281                                              $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey);
 282                                              $plotAttributes = $this->readChartAttributes($chartDetail);
 283  
 284                                              break;
 285                                          case 'areaChart':
 286                                          case 'area3DChart':
 287                                              $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey);
 288                                              $plotAttributes = $this->readChartAttributes($chartDetail);
 289  
 290                                              break;
 291                                          case 'doughnutChart':
 292                                          case 'pieChart':
 293                                          case 'pie3DChart':
 294                                              $explosion = self::getAttribute($chartDetail->ser->explosion, 'val', 'string');
 295                                              $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey);
 296                                              $plotSer->setPlotStyle("$explosion");
 297                                              $plotSeries[] = $plotSer;
 298                                              $plotAttributes = $this->readChartAttributes($chartDetail);
 299  
 300                                              break;
 301                                          case 'scatterChart':
 302                                              /** @var string */
 303                                              $scatterStyle = self::getAttribute($chartDetail->scatterStyle, 'val', 'string');
 304                                              $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey);
 305                                              $plotSer->setPlotStyle($scatterStyle);
 306                                              $plotSeries[] = $plotSer;
 307                                              $plotAttributes = $this->readChartAttributes($chartDetail);
 308  
 309                                              break;
 310                                          case 'bubbleChart':
 311                                              $bubbleScale = self::getAttribute($chartDetail->bubbleScale, 'val', 'integer');
 312                                              $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey);
 313                                              $plotSer->setPlotStyle("$bubbleScale");
 314                                              $plotSeries[] = $plotSer;
 315                                              $plotAttributes = $this->readChartAttributes($chartDetail);
 316  
 317                                              break;
 318                                          case 'radarChart':
 319                                              /** @var string */
 320                                              $radarStyle = self::getAttribute($chartDetail->radarStyle, 'val', 'string');
 321                                              $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey);
 322                                              $plotSer->setPlotStyle($radarStyle);
 323                                              $plotSeries[] = $plotSer;
 324                                              $plotAttributes = $this->readChartAttributes($chartDetail);
 325  
 326                                              break;
 327                                          case 'surfaceChart':
 328                                          case 'surface3DChart':
 329                                              $wireFrame = self::getAttribute($chartDetail->wireframe, 'val', 'boolean');
 330                                              $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey);
 331                                              $plotSer->setPlotStyle("$wireFrame");
 332                                              $plotSeries[] = $plotSer;
 333                                              $plotAttributes = $this->readChartAttributes($chartDetail);
 334  
 335                                              break;
 336                                          case 'stockChart':
 337                                              $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey);
 338                                              if (isset($chartDetail->upDownBars->gapWidth)) {
 339                                                  $gapWidth = self::getAttribute($chartDetail->upDownBars->gapWidth, 'val', 'integer');
 340                                              }
 341                                              if (isset($chartDetail->upDownBars->upBars)) {
 342                                                  $useUpBars = true;
 343                                              }
 344                                              if (isset($chartDetail->upDownBars->downBars)) {
 345                                                  $useDownBars = true;
 346                                              }
 347                                              $plotAttributes = $this->readChartAttributes($chartDetail);
 348  
 349                                              break;
 350                                      }
 351                                  }
 352                                  if ($plotAreaLayout == null) {
 353                                      $plotAreaLayout = new Layout();
 354                                  }
 355                                  $plotArea = new PlotArea($plotAreaLayout, $plotSeries);
 356                                  $this->setChartAttributes($plotAreaLayout, $plotAttributes);
 357                                  if ($plotNoFill) {
 358                                      $plotArea->setNoFill(true);
 359                                  }
 360                                  if (!empty($gradientArray)) {
 361                                      $plotArea->setGradientFillProperties($gradientArray, $gradientLin);
 362                                  }
 363                                  if (is_int($gapWidth)) {
 364                                      $plotArea->setGapWidth($gapWidth);
 365                                  }
 366                                  if ($useUpBars === true) {
 367                                      $plotArea->setUseUpBars(true);
 368                                  }
 369                                  if ($useDownBars === true) {
 370                                      $plotArea->setUseDownBars(true);
 371                                  }
 372  
 373                                  break;
 374                              case 'plotVisOnly':
 375                                  $plotVisOnly = self::getAttribute($chartDetails, 'val', 'string');
 376  
 377                                  break;
 378                              case 'dispBlanksAs':
 379                                  $dispBlanksAs = self::getAttribute($chartDetails, 'val', 'string');
 380  
 381                                  break;
 382                              case 'title':
 383                                  $title = $this->chartTitle($chartDetails);
 384  
 385                                  break;
 386                              case 'legend':
 387                                  $legendPos = 'r';
 388                                  $legendLayout = null;
 389                                  $legendOverlay = false;
 390                                  $legendBorderLines = null;
 391                                  $legendFillColor = null;
 392                                  $legendText = null;
 393                                  $addLegendText = false;
 394                                  foreach ($chartDetails as $chartDetailKey => $chartDetail) {
 395                                      $chartDetail = Xlsx::testSimpleXml($chartDetail);
 396                                      switch ($chartDetailKey) {
 397                                          case 'legendPos':
 398                                              $legendPos = self::getAttribute($chartDetail, 'val', 'string');
 399  
 400                                              break;
 401                                          case 'overlay':
 402                                              $legendOverlay = self::getAttribute($chartDetail, 'val', 'boolean');
 403  
 404                                              break;
 405                                          case 'layout':
 406                                              $legendLayout = $this->chartLayoutDetails($chartDetail);
 407  
 408                                              break;
 409                                          case 'spPr':
 410                                              $children = $chartDetails->spPr->children($this->aNamespace);
 411                                              if (isset($children->solidFill)) {
 412                                                  $legendFillColor = $this->readColor($children->solidFill);
 413                                              }
 414                                              if (isset($children->ln)) {
 415                                                  $legendBorderLines = new GridLines();
 416                                                  $this->readLineStyle($chartDetails, $legendBorderLines);
 417                                              }
 418  
 419                                              break;
 420                                          case 'txPr':
 421                                              $children = $chartDetails->txPr->children($this->aNamespace);
 422                                              $addLegendText = false;
 423                                              $legendText = new AxisText();
 424                                              if (isset($children->p->pPr->defRPr->solidFill)) {
 425                                                  $colorArray = $this->readColor($children->p->pPr->defRPr->solidFill);
 426                                                  $legendText->getFillColorObject()->setColorPropertiesArray($colorArray);
 427                                                  $addLegendText = true;
 428                                              }
 429                                              if (isset($children->p->pPr->defRPr->effectLst)) {
 430                                                  $this->readEffects($children->p->pPr->defRPr, $legendText, false);
 431                                                  $addLegendText = true;
 432                                              }
 433  
 434                                              break;
 435                                      }
 436                                  }
 437                                  $legend = new Legend("$legendPos", $legendLayout, (bool) $legendOverlay);
 438                                  if ($legendFillColor !== null) {
 439                                      $legend->getFillColor()->setColorPropertiesArray($legendFillColor);
 440                                  }
 441                                  if ($legendBorderLines !== null) {
 442                                      $legend->setBorderLines($legendBorderLines);
 443                                  }
 444                                  if ($addLegendText) {
 445                                      $legend->setLegendText($legendText);
 446                                  }
 447  
 448                                  break;
 449                          }
 450                      }
 451              }
 452          }
 453          $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel, $xAxis, $yAxis);
 454          if ($chartNoFill) {
 455              $chart->setNoFill(true);
 456          }
 457          if ($chartFillColor !== null) {
 458              $chart->getFillColor()->setColorPropertiesArray($chartFillColor);
 459          }
 460          if ($chartBorderLines !== null) {
 461              $chart->setBorderLines($chartBorderLines);
 462          }
 463          $chart->setRoundedCorners($roundedCorners);
 464          if (is_bool($autoTitleDeleted)) {
 465              $chart->setAutoTitleDeleted($autoTitleDeleted);
 466          }
 467          if (is_int($rotX)) {
 468              $chart->setRotX($rotX);
 469          }
 470          if (is_int($rotY)) {
 471              $chart->setRotY($rotY);
 472          }
 473          if (is_int($rAngAx)) {
 474              $chart->setRAngAx($rAngAx);
 475          }
 476          if (is_int($perspective)) {
 477              $chart->setPerspective($perspective);
 478          }
 479  
 480          return $chart;
 481      }
 482  
 483      private function chartTitle(SimpleXMLElement $titleDetails): Title
 484      {
 485          $caption = [];
 486          $titleLayout = null;
 487          $titleOverlay = false;
 488          foreach ($titleDetails as $titleDetailKey => $chartDetail) {
 489              $chartDetail = Xlsx::testSimpleXml($chartDetail);
 490              switch ($titleDetailKey) {
 491                  case 'tx':
 492                      if (isset($chartDetail->rich)) {
 493                          $titleDetails = $chartDetail->rich->children($this->aNamespace);
 494                          foreach ($titleDetails as $titleKey => $titleDetail) {
 495                              $titleDetail = Xlsx::testSimpleXml($titleDetail);
 496                              switch ($titleKey) {
 497                                  case 'p':
 498                                      $titleDetailPart = $titleDetail->children($this->aNamespace);
 499                                      $caption[] = $this->parseRichText($titleDetailPart);
 500                              }
 501                          }
 502                      } elseif (isset($chartDetail->strRef->strCache)) {
 503                          foreach ($chartDetail->strRef->strCache->pt as $pt) {
 504                              if (isset($pt->v)) {
 505                                  $caption[] = (string) $pt->v;
 506                              }
 507                          }
 508                      }
 509  
 510                      break;
 511                  case 'overlay':
 512                      $titleOverlay = self::getAttribute($chartDetail, 'val', 'boolean');
 513  
 514                      break;
 515                  case 'layout':
 516                      $titleLayout = $this->chartLayoutDetails($chartDetail);
 517  
 518                      break;
 519              }
 520          }
 521  
 522          return new Title($caption, $titleLayout, (bool) $titleOverlay);
 523      }
 524  
 525      private function chartLayoutDetails(SimpleXMLElement $chartDetail): ?Layout
 526      {
 527          if (!isset($chartDetail->manualLayout)) {
 528              return null;
 529          }
 530          $details = $chartDetail->manualLayout->children($this->cNamespace);
 531          if ($details === null) {
 532              return null;
 533          }
 534          $layout = [];
 535          foreach ($details as $detailKey => $detail) {
 536              $detail = Xlsx::testSimpleXml($detail);
 537              $layout[$detailKey] = self::getAttribute($detail, 'val', 'string');
 538          }
 539  
 540          return new Layout($layout);
 541      }
 542  
 543      private function chartDataSeries(SimpleXMLElement $chartDetail, string $plotType): DataSeries
 544      {
 545          $multiSeriesType = null;
 546          $smoothLine = false;
 547          $seriesLabel = $seriesCategory = $seriesValues = $plotOrder = $seriesBubbles = [];
 548  
 549          $seriesDetailSet = $chartDetail->children($this->cNamespace);
 550          foreach ($seriesDetailSet as $seriesDetailKey => $seriesDetails) {
 551              switch ($seriesDetailKey) {
 552                  case 'grouping':
 553                      $multiSeriesType = self::getAttribute($chartDetail->grouping, 'val', 'string');
 554  
 555                      break;
 556                  case 'ser':
 557                      $marker = null;
 558                      $seriesIndex = '';
 559                      $fillColor = null;
 560                      $pointSize = null;
 561                      $noFill = false;
 562                      $bubble3D = false;
 563                      $dptColors = [];
 564                      $markerFillColor = null;
 565                      $markerBorderColor = null;
 566                      $lineStyle = null;
 567                      $labelLayout = null;
 568                      $trendLines = [];
 569                      foreach ($seriesDetails as $seriesKey => $seriesDetail) {
 570                          $seriesDetail = Xlsx::testSimpleXml($seriesDetail);
 571                          switch ($seriesKey) {
 572                              case 'idx':
 573                                  $seriesIndex = self::getAttribute($seriesDetail, 'val', 'integer');
 574  
 575                                  break;
 576                              case 'order':
 577                                  $seriesOrder = self::getAttribute($seriesDetail, 'val', 'integer');
 578                                  $plotOrder[$seriesIndex] = $seriesOrder;
 579  
 580                                  break;
 581                              case 'tx':
 582                                  $seriesLabel[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail);
 583  
 584                                  break;
 585                              case 'spPr':
 586                                  $children = $seriesDetail->children($this->aNamespace);
 587                                  if (isset($children->ln)) {
 588                                      $ln = $children->ln;
 589                                      if (is_countable($ln->noFill) && count($ln->noFill) === 1) {
 590                                          $noFill = true;
 591                                      }
 592                                      $lineStyle = new GridLines();
 593                                      $this->readLineStyle($seriesDetails, $lineStyle);
 594                                  }
 595                                  if (isset($children->effectLst)) {
 596                                      if ($lineStyle === null) {
 597                                          $lineStyle = new GridLines();
 598                                      }
 599                                      $this->readEffects($seriesDetails, $lineStyle);
 600                                  }
 601                                  if (isset($children->solidFill)) {
 602                                      $fillColor = new ChartColor($this->readColor($children->solidFill));
 603                                  }
 604  
 605                                  break;
 606                              case 'dPt':
 607                                  $dptIdx = (int) self::getAttribute($seriesDetail->idx, 'val', 'string');
 608                                  if (isset($seriesDetail->spPr)) {
 609                                      $children = $seriesDetail->spPr->children($this->aNamespace);
 610                                      if (isset($children->solidFill)) {
 611                                          $arrayColors = $this->readColor($children->solidFill);
 612                                          $dptColors[$dptIdx] = new ChartColor($arrayColors);
 613                                      }
 614                                  }
 615  
 616                                  break;
 617                              case 'trendline':
 618                                  $trendLine = new TrendLine();
 619                                  $this->readLineStyle($seriesDetail, $trendLine);
 620                                  /** @var ?string */
 621                                  $trendLineType = self::getAttribute($seriesDetail->trendlineType, 'val', 'string');
 622                                  /** @var ?bool */
 623                                  $dispRSqr = self::getAttribute($seriesDetail->dispRSqr, 'val', 'boolean');
 624                                  /** @var ?bool */
 625                                  $dispEq = self::getAttribute($seriesDetail->dispEq, 'val', 'boolean');
 626                                  /** @var ?int */
 627                                  $order = self::getAttribute($seriesDetail->order, 'val', 'integer');
 628                                  /** @var ?int */
 629                                  $period = self::getAttribute($seriesDetail->period, 'val', 'integer');
 630                                  /** @var ?float */
 631                                  $forward = self::getAttribute($seriesDetail->forward, 'val', 'float');
 632                                  /** @var ?float */
 633                                  $backward = self::getAttribute($seriesDetail->backward, 'val', 'float');
 634                                  /** @var ?float */
 635                                  $intercept = self::getAttribute($seriesDetail->intercept, 'val', 'float');
 636                                  /** @var ?string */
 637                                  $name = (string) $seriesDetail->name;
 638                                  $trendLine->setTrendLineProperties(
 639                                      $trendLineType,
 640                                      $order,
 641                                      $period,
 642                                      $dispRSqr,
 643                                      $dispEq,
 644                                      $backward,
 645                                      $forward,
 646                                      $intercept,
 647                                      $name
 648                                  );
 649                                  $trendLines[] = $trendLine;
 650  
 651                                  break;
 652                              case 'marker':
 653                                  $marker = self::getAttribute($seriesDetail->symbol, 'val', 'string');
 654                                  $pointSize = self::getAttribute($seriesDetail->size, 'val', 'string');
 655                                  $pointSize = is_numeric($pointSize) ? ((int) $pointSize) : null;
 656                                  if (isset($seriesDetail->spPr)) {
 657                                      $children = $seriesDetail->spPr->children($this->aNamespace);
 658                                      if (isset($children->solidFill)) {
 659                                          $markerFillColor = $this->readColor($children->solidFill);
 660                                      }
 661                                      if (isset($children->ln->solidFill)) {
 662                                          $markerBorderColor = $this->readColor($children->ln->solidFill);
 663                                      }
 664                                  }
 665  
 666                                  break;
 667                              case 'smooth':
 668                                  $smoothLine = self::getAttribute($seriesDetail, 'val', 'boolean');
 669  
 670                                  break;
 671                              case 'cat':
 672                                  $seriesCategory[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail);
 673  
 674                                  break;
 675                              case 'val':
 676                                  $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize");
 677  
 678                                  break;
 679                              case 'xVal':
 680                                  $seriesCategory[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize");
 681  
 682                                  break;
 683                              case 'yVal':
 684                                  $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize");
 685  
 686                                  break;
 687                              case 'bubbleSize':
 688                                  $seriesBubbles[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize");
 689  
 690                                  break;
 691                              case 'bubble3D':
 692                                  $bubble3D = self::getAttribute($seriesDetail, 'val', 'boolean');
 693  
 694                                  break;
 695                              case 'dLbls':
 696                                  $labelLayout = new Layout($this->readChartAttributes($seriesDetails));
 697  
 698                                  break;
 699                          }
 700                      }
 701                      if ($labelLayout) {
 702                          if (isset($seriesLabel[$seriesIndex])) {
 703                              $seriesLabel[$seriesIndex]->setLabelLayout($labelLayout);
 704                          }
 705                          if (isset($seriesCategory[$seriesIndex])) {
 706                              $seriesCategory[$seriesIndex]->setLabelLayout($labelLayout);
 707                          }
 708                          if (isset($seriesValues[$seriesIndex])) {
 709                              $seriesValues[$seriesIndex]->setLabelLayout($labelLayout);
 710                          }
 711                      }
 712                      if ($noFill) {
 713                          if (isset($seriesLabel[$seriesIndex])) {
 714                              $seriesLabel[$seriesIndex]->setScatterLines(false);
 715                          }
 716                          if (isset($seriesCategory[$seriesIndex])) {
 717                              $seriesCategory[$seriesIndex]->setScatterLines(false);
 718                          }
 719                          if (isset($seriesValues[$seriesIndex])) {
 720                              $seriesValues[$seriesIndex]->setScatterLines(false);
 721                          }
 722                      }
 723                      if ($lineStyle !== null) {
 724                          if (isset($seriesLabel[$seriesIndex])) {
 725                              $seriesLabel[$seriesIndex]->copyLineStyles($lineStyle);
 726                          }
 727                          if (isset($seriesCategory[$seriesIndex])) {
 728                              $seriesCategory[$seriesIndex]->copyLineStyles($lineStyle);
 729                          }
 730                          if (isset($seriesValues[$seriesIndex])) {
 731                              $seriesValues[$seriesIndex]->copyLineStyles($lineStyle);
 732                          }
 733                      }
 734                      if ($bubble3D) {
 735                          if (isset($seriesLabel[$seriesIndex])) {
 736                              $seriesLabel[$seriesIndex]->setBubble3D($bubble3D);
 737                          }
 738                          if (isset($seriesCategory[$seriesIndex])) {
 739                              $seriesCategory[$seriesIndex]->setBubble3D($bubble3D);
 740                          }
 741                          if (isset($seriesValues[$seriesIndex])) {
 742                              $seriesValues[$seriesIndex]->setBubble3D($bubble3D);
 743                          }
 744                      }
 745                      if (!empty($dptColors)) {
 746                          if (isset($seriesLabel[$seriesIndex])) {
 747                              $seriesLabel[$seriesIndex]->setFillColor($dptColors);
 748                          }
 749                          if (isset($seriesCategory[$seriesIndex])) {
 750                              $seriesCategory[$seriesIndex]->setFillColor($dptColors);
 751                          }
 752                          if (isset($seriesValues[$seriesIndex])) {
 753                              $seriesValues[$seriesIndex]->setFillColor($dptColors);
 754                          }
 755                      }
 756                      if ($markerFillColor !== null) {
 757                          if (isset($seriesLabel[$seriesIndex])) {
 758                              $seriesLabel[$seriesIndex]->getMarkerFillColor()->setColorPropertiesArray($markerFillColor);
 759                          }
 760                          if (isset($seriesCategory[$seriesIndex])) {
 761                              $seriesCategory[$seriesIndex]->getMarkerFillColor()->setColorPropertiesArray($markerFillColor);
 762                          }
 763                          if (isset($seriesValues[$seriesIndex])) {
 764                              $seriesValues[$seriesIndex]->getMarkerFillColor()->setColorPropertiesArray($markerFillColor);
 765                          }
 766                      }
 767                      if ($markerBorderColor !== null) {
 768                          if (isset($seriesLabel[$seriesIndex])) {
 769                              $seriesLabel[$seriesIndex]->getMarkerBorderColor()->setColorPropertiesArray($markerBorderColor);
 770                          }
 771                          if (isset($seriesCategory[$seriesIndex])) {
 772                              $seriesCategory[$seriesIndex]->getMarkerBorderColor()->setColorPropertiesArray($markerBorderColor);
 773                          }
 774                          if (isset($seriesValues[$seriesIndex])) {
 775                              $seriesValues[$seriesIndex]->getMarkerBorderColor()->setColorPropertiesArray($markerBorderColor);
 776                          }
 777                      }
 778                      if ($smoothLine) {
 779                          if (isset($seriesLabel[$seriesIndex])) {
 780                              $seriesLabel[$seriesIndex]->setSmoothLine(true);
 781                          }
 782                          if (isset($seriesCategory[$seriesIndex])) {
 783                              $seriesCategory[$seriesIndex]->setSmoothLine(true);
 784                          }
 785                          if (isset($seriesValues[$seriesIndex])) {
 786                              $seriesValues[$seriesIndex]->setSmoothLine(true);
 787                          }
 788                      }
 789                      if (!empty($trendLines)) {
 790                          if (isset($seriesLabel[$seriesIndex])) {
 791                              $seriesLabel[$seriesIndex]->setTrendLines($trendLines);
 792                          }
 793                          if (isset($seriesCategory[$seriesIndex])) {
 794                              $seriesCategory[$seriesIndex]->setTrendLines($trendLines);
 795                          }
 796                          if (isset($seriesValues[$seriesIndex])) {
 797                              $seriesValues[$seriesIndex]->setTrendLines($trendLines);
 798                          }
 799                      }
 800              }
 801          }
 802          /** @phpstan-ignore-next-line */
 803          $series = new DataSeries($plotType, $multiSeriesType, $plotOrder, $seriesLabel, $seriesCategory, $seriesValues, $smoothLine);
 804          $series->setPlotBubbleSizes($seriesBubbles);
 805  
 806          return $series;
 807      }
 808  
 809      /**
 810       * @return mixed
 811       */
 812      private function chartDataSeriesValueSet(SimpleXMLElement $seriesDetail, ?string $marker = null, ?ChartColor $fillColor = null, ?string $pointSize = null)
 813      {
 814          if (isset($seriesDetail->strRef)) {
 815              $seriesSource = (string) $seriesDetail->strRef->f;
 816              $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize");
 817  
 818              if (isset($seriesDetail->strRef->strCache)) {
 819                  $seriesData = $this->chartDataSeriesValues($seriesDetail->strRef->strCache->children($this->cNamespace), 's');
 820                  $seriesValues
 821                      ->setFormatCode($seriesData['formatCode'])
 822                      ->setDataValues($seriesData['dataValues']);
 823              }
 824  
 825              return $seriesValues;
 826          } elseif (isset($seriesDetail->numRef)) {
 827              $seriesSource = (string) $seriesDetail->numRef->f;
 828              $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize");
 829              if (isset($seriesDetail->numRef->numCache)) {
 830                  $seriesData = $this->chartDataSeriesValues($seriesDetail->numRef->numCache->children($this->cNamespace));
 831                  $seriesValues
 832                      ->setFormatCode($seriesData['formatCode'])
 833                      ->setDataValues($seriesData['dataValues']);
 834              }
 835  
 836              return $seriesValues;
 837          } elseif (isset($seriesDetail->multiLvlStrRef)) {
 838              $seriesSource = (string) $seriesDetail->multiLvlStrRef->f;
 839              $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize");
 840  
 841              if (isset($seriesDetail->multiLvlStrRef->multiLvlStrCache)) {
 842                  $seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($this->cNamespace), 's');
 843                  $seriesValues
 844                      ->setFormatCode($seriesData['formatCode'])
 845                      ->setDataValues($seriesData['dataValues']);
 846              }
 847  
 848              return $seriesValues;
 849          } elseif (isset($seriesDetail->multiLvlNumRef)) {
 850              $seriesSource = (string) $seriesDetail->multiLvlNumRef->f;
 851              $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize");
 852  
 853              if (isset($seriesDetail->multiLvlNumRef->multiLvlNumCache)) {
 854                  $seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($this->cNamespace), 's');
 855                  $seriesValues
 856                      ->setFormatCode($seriesData['formatCode'])
 857                      ->setDataValues($seriesData['dataValues']);
 858              }
 859  
 860              return $seriesValues;
 861          }
 862  
 863          if (isset($seriesDetail->v)) {
 864              return new DataSeriesValues(
 865                  DataSeriesValues::DATASERIES_TYPE_STRING,
 866                  null,
 867                  null,
 868                  1,
 869                  [(string) $seriesDetail->v]
 870              );
 871          }
 872  
 873          return null;
 874      }
 875  
 876      private function chartDataSeriesValues(SimpleXMLElement $seriesValueSet, string $dataType = 'n'): array
 877      {
 878          $seriesVal = [];
 879          $formatCode = '';
 880          $pointCount = 0;
 881  
 882          foreach ($seriesValueSet as $seriesValueIdx => $seriesValue) {
 883              $seriesValue = Xlsx::testSimpleXml($seriesValue);
 884              switch ($seriesValueIdx) {
 885                  case 'ptCount':
 886                      $pointCount = self::getAttribute($seriesValue, 'val', 'integer');
 887  
 888                      break;
 889                  case 'formatCode':
 890                      $formatCode = (string) $seriesValue;
 891  
 892                      break;
 893                  case 'pt':
 894                      $pointVal = self::getAttribute($seriesValue, 'idx', 'integer');
 895                      if ($dataType == 's') {
 896                          $seriesVal[$pointVal] = (string) $seriesValue->v;
 897                      } elseif ((string) $seriesValue->v === ExcelError::NA()) {
 898                          $seriesVal[$pointVal] = null;
 899                      } else {
 900                          $seriesVal[$pointVal] = (float) $seriesValue->v;
 901                      }
 902  
 903                      break;
 904              }
 905          }
 906  
 907          return [
 908              'formatCode' => $formatCode,
 909              'pointCount' => $pointCount,
 910              'dataValues' => $seriesVal,
 911          ];
 912      }
 913  
 914      private function chartDataSeriesValuesMultiLevel(SimpleXMLElement $seriesValueSet, string $dataType = 'n'): array
 915      {
 916          $seriesVal = [];
 917          $formatCode = '';
 918          $pointCount = 0;
 919  
 920          foreach ($seriesValueSet->lvl as $seriesLevelIdx => $seriesLevel) {
 921              foreach ($seriesLevel as $seriesValueIdx => $seriesValue) {
 922                  $seriesValue = Xlsx::testSimpleXml($seriesValue);
 923                  switch ($seriesValueIdx) {
 924                      case 'ptCount':
 925                          $pointCount = self::getAttribute($seriesValue, 'val', 'integer');
 926  
 927                          break;
 928                      case 'formatCode':
 929                          $formatCode = (string) $seriesValue;
 930  
 931                          break;
 932                      case 'pt':
 933                          $pointVal = self::getAttribute($seriesValue, 'idx', 'integer');
 934                          if ($dataType == 's') {
 935                              $seriesVal[$pointVal][] = (string) $seriesValue->v;
 936                          } elseif ((string) $seriesValue->v === ExcelError::NA()) {
 937                              $seriesVal[$pointVal] = null;
 938                          } else {
 939                              $seriesVal[$pointVal][] = (float) $seriesValue->v;
 940                          }
 941  
 942                          break;
 943                  }
 944              }
 945          }
 946  
 947          return [
 948              'formatCode' => $formatCode,
 949              'pointCount' => $pointCount,
 950              'dataValues' => $seriesVal,
 951          ];
 952      }
 953  
 954      private function parseRichText(SimpleXMLElement $titleDetailPart): RichText
 955      {
 956          $value = new RichText();
 957          $defaultFontSize = null;
 958          $defaultBold = null;
 959          $defaultItalic = null;
 960          $defaultUnderscore = null;
 961          $defaultStrikethrough = null;
 962          $defaultBaseline = null;
 963          $defaultFontName = null;
 964          $defaultLatin = null;
 965          $defaultEastAsian = null;
 966          $defaultComplexScript = null;
 967          $defaultFontColor = null;
 968          if (isset($titleDetailPart->pPr->defRPr)) {
 969              /** @var ?int */
 970              $defaultFontSize = self::getAttribute($titleDetailPart->pPr->defRPr, 'sz', 'integer');
 971              /** @var ?bool */
 972              $defaultBold = self::getAttribute($titleDetailPart->pPr->defRPr, 'b', 'boolean');
 973              /** @var ?bool */
 974              $defaultItalic = self::getAttribute($titleDetailPart->pPr->defRPr, 'i', 'boolean');
 975              /** @var ?string */
 976              $defaultUnderscore = self::getAttribute($titleDetailPart->pPr->defRPr, 'u', 'string');
 977              /** @var ?string */
 978              $defaultStrikethrough = self::getAttribute($titleDetailPart->pPr->defRPr, 'strike', 'string');
 979              /** @var ?int */
 980              $defaultBaseline = self::getAttribute($titleDetailPart->pPr->defRPr, 'baseline', 'integer');
 981              if (isset($titleDetailPart->defRPr->rFont['val'])) {
 982                  $defaultFontName = (string) $titleDetailPart->defRPr->rFont['val'];
 983              }
 984              if (isset($titleDetailPart->pPr->defRPr->latin)) {
 985                  /** @var ?string */
 986                  $defaultLatin = self::getAttribute($titleDetailPart->pPr->defRPr->latin, 'typeface', 'string');
 987              }
 988              if (isset($titleDetailPart->pPr->defRPr->ea)) {
 989                  /** @var ?string */
 990                  $defaultEastAsian = self::getAttribute($titleDetailPart->pPr->defRPr->ea, 'typeface', 'string');
 991              }
 992              if (isset($titleDetailPart->pPr->defRPr->cs)) {
 993                  /** @var ?string */
 994                  $defaultComplexScript = self::getAttribute($titleDetailPart->pPr->defRPr->cs, 'typeface', 'string');
 995              }
 996              if (isset($titleDetailPart->pPr->defRPr->solidFill)) {
 997                  $defaultFontColor = $this->readColor($titleDetailPart->pPr->defRPr->solidFill);
 998              }
 999          }
1000          foreach ($titleDetailPart as $titleDetailElementKey => $titleDetailElement) {
1001              if (
1002                  (string) $titleDetailElementKey !== 'r'
1003                  || !isset($titleDetailElement->t)
1004              ) {
1005                  continue;
1006              }
1007              $objText = $value->createTextRun((string) $titleDetailElement->t);
1008              if ($objText->getFont() === null) {
1009                  // @codeCoverageIgnoreStart
1010                  continue;
1011                  // @codeCoverageIgnoreEnd
1012              }
1013              $fontSize = null;
1014              $bold = null;
1015              $italic = null;
1016              $underscore = null;
1017              $strikethrough = null;
1018              $baseline = null;
1019              $fontName = null;
1020              $latinName = null;
1021              $eastAsian = null;
1022              $complexScript = null;
1023              $fontColor = null;
1024              $underlineColor = null;
1025              if (isset($titleDetailElement->rPr)) {
1026                  // not used now, not sure it ever was, grandfathering
1027                  if (isset($titleDetailElement->rPr->rFont['val'])) {
1028                      // @codeCoverageIgnoreStart
1029                      $fontName = (string) $titleDetailElement->rPr->rFont['val'];
1030                      // @codeCoverageIgnoreEnd
1031                  }
1032                  if (isset($titleDetailElement->rPr->latin)) {
1033                      /** @var ?string */
1034                      $latinName = self::getAttribute($titleDetailElement->rPr->latin, 'typeface', 'string');
1035                  }
1036                  if (isset($titleDetailElement->rPr->ea)) {
1037                      /** @var ?string */
1038                      $eastAsian = self::getAttribute($titleDetailElement->rPr->ea, 'typeface', 'string');
1039                  }
1040                  if (isset($titleDetailElement->rPr->cs)) {
1041                      /** @var ?string */
1042                      $complexScript = self::getAttribute($titleDetailElement->rPr->cs, 'typeface', 'string');
1043                  }
1044                  /** @var ?int */
1045                  $fontSize = self::getAttribute($titleDetailElement->rPr, 'sz', 'integer');
1046  
1047                  // not used now, not sure it ever was, grandfathering
1048                  if (isset($titleDetailElement->rPr->solidFill)) {
1049                      $fontColor = $this->readColor($titleDetailElement->rPr->solidFill);
1050                  }
1051  
1052                  /** @var ?bool */
1053                  $bold = self::getAttribute($titleDetailElement->rPr, 'b', 'boolean');
1054  
1055                  /** @var ?bool */
1056                  $italic = self::getAttribute($titleDetailElement->rPr, 'i', 'boolean');
1057  
1058                  /** @var ?int */
1059                  $baseline = self::getAttribute($titleDetailElement->rPr, 'baseline', 'integer');
1060  
1061                  /** @var ?string */
1062                  $underscore = self::getAttribute($titleDetailElement->rPr, 'u', 'string');
1063                  if (isset($titleDetailElement->rPr->uFill->solidFill)) {
1064                      $underlineColor = $this->readColor($titleDetailElement->rPr->uFill->solidFill);
1065                  }
1066  
1067                  /** @var ?string */
1068                  $strikethrough = self::getAttribute($titleDetailElement->rPr, 'strike', 'string');
1069              }
1070  
1071              $fontFound = false;
1072              $latinName = $latinName ?? $defaultLatin;
1073              if ($latinName !== null) {
1074                  $objText->getFont()->setLatin($latinName);
1075                  $fontFound = true;
1076              }
1077              $eastAsian = $eastAsian ?? $defaultEastAsian;
1078              if ($eastAsian !== null) {
1079                  $objText->getFont()->setEastAsian($eastAsian);
1080                  $fontFound = true;
1081              }
1082              $complexScript = $complexScript ?? $defaultComplexScript;
1083              if ($complexScript !== null) {
1084                  $objText->getFont()->setComplexScript($complexScript);
1085                  $fontFound = true;
1086              }
1087              $fontName = $fontName ?? $defaultFontName;
1088              if ($fontName !== null) {
1089                  // @codeCoverageIgnoreStart
1090                  $objText->getFont()->setName($fontName);
1091                  $fontFound = true;
1092                  // @codeCoverageIgnoreEnd
1093              }
1094  
1095              $fontSize = $fontSize ?? $defaultFontSize;
1096              if (is_int($fontSize)) {
1097                  $objText->getFont()->setSize(floor($fontSize / 100));
1098                  $fontFound = true;
1099              } else {
1100                  $objText->getFont()->setSize(null, true);
1101              }
1102  
1103              $fontColor = $fontColor ?? $defaultFontColor;
1104              if (!empty($fontColor)) {
1105                  $objText->getFont()->setChartColor($fontColor);
1106                  $fontFound = true;
1107              }
1108  
1109              $bold = $bold ?? $defaultBold;
1110              if ($bold !== null) {
1111                  $objText->getFont()->setBold($bold);
1112                  $fontFound = true;
1113              }
1114  
1115              $italic = $italic ?? $defaultItalic;
1116              if ($italic !== null) {
1117                  $objText->getFont()->setItalic($italic);
1118                  $fontFound = true;
1119              }
1120  
1121              $baseline = $baseline ?? $defaultBaseline;
1122              if ($baseline !== null) {
1123                  $objText->getFont()->setBaseLine($baseline);
1124                  if ($baseline > 0) {
1125                      $objText->getFont()->setSuperscript(true);
1126                  } elseif ($baseline < 0) {
1127                      $objText->getFont()->setSubscript(true);
1128                  }
1129                  $fontFound = true;
1130              }
1131  
1132              $underscore = $underscore ?? $defaultUnderscore;
1133              if ($underscore !== null) {
1134                  if ($underscore == 'sng') {
1135                      $objText->getFont()->setUnderline(Font::UNDERLINE_SINGLE);
1136                  } elseif ($underscore == 'dbl') {
1137                      $objText->getFont()->setUnderline(Font::UNDERLINE_DOUBLE);
1138                  } elseif ($underscore !== '') {
1139                      $objText->getFont()->setUnderline($underscore);
1140                  } else {
1141                      $objText->getFont()->setUnderline(Font::UNDERLINE_NONE);
1142                  }
1143                  $fontFound = true;
1144                  if ($underlineColor) {
1145                      $objText->getFont()->setUnderlineColor($underlineColor);
1146                  }
1147              }
1148  
1149              $strikethrough = $strikethrough ?? $defaultStrikethrough;
1150              if ($strikethrough !== null) {
1151                  $objText->getFont()->setStrikeType($strikethrough);
1152                  if ($strikethrough == 'noStrike') {
1153                      $objText->getFont()->setStrikethrough(false);
1154                  } else {
1155                      $objText->getFont()->setStrikethrough(true);
1156                  }
1157                  $fontFound = true;
1158              }
1159              if ($fontFound === false) {
1160                  $objText->setFont(null);
1161              }
1162          }
1163  
1164          return $value;
1165      }
1166  
1167      private function parseFont(SimpleXMLElement $titleDetailPart): ?Font
1168      {
1169          if (!isset($titleDetailPart->pPr->defRPr)) {
1170              return null;
1171          }
1172          $fontArray = [];
1173          $fontArray['size'] = self::getAttribute($titleDetailPart->pPr->defRPr, 'sz', 'integer');
1174          $fontArray['bold'] = self::getAttribute($titleDetailPart->pPr->defRPr, 'b', 'boolean');
1175          $fontArray['italic'] = self::getAttribute($titleDetailPart->pPr->defRPr, 'i', 'boolean');
1176          $fontArray['underscore'] = self::getAttribute($titleDetailPart->pPr->defRPr, 'u', 'string');
1177          $fontArray['strikethrough'] = self::getAttribute($titleDetailPart->pPr->defRPr, 'strike', 'string');
1178  
1179          if (isset($titleDetailPart->pPr->defRPr->latin)) {
1180              $fontArray['latin'] = self::getAttribute($titleDetailPart->pPr->defRPr->latin, 'typeface', 'string');
1181          }
1182          if (isset($titleDetailPart->pPr->defRPr->ea)) {
1183              $fontArray['eastAsian'] = self::getAttribute($titleDetailPart->pPr->defRPr->ea, 'typeface', 'string');
1184          }
1185          if (isset($titleDetailPart->pPr->defRPr->cs)) {
1186              $fontArray['complexScript'] = self::getAttribute($titleDetailPart->pPr->defRPr->cs, 'typeface', 'string');
1187          }
1188          if (isset($titleDetailPart->pPr->defRPr->solidFill)) {
1189              $fontArray['chartColor'] = new ChartColor($this->readColor($titleDetailPart->pPr->defRPr->solidFill));
1190          }
1191          $font = new Font();
1192          $font->setSize(null, true);
1193          $font->applyFromArray($fontArray);
1194  
1195          return $font;
1196      }
1197  
1198      /**
1199       * @param ?SimpleXMLElement $chartDetail
1200       */
1201      private function readChartAttributes($chartDetail): array
1202      {
1203          $plotAttributes = [];
1204          if (isset($chartDetail->dLbls)) {
1205              if (isset($chartDetail->dLbls->dLblPos)) {
1206                  $plotAttributes['dLblPos'] = self::getAttribute($chartDetail->dLbls->dLblPos, 'val', 'string');
1207              }
1208              if (isset($chartDetail->dLbls->numFmt)) {
1209                  $plotAttributes['numFmtCode'] = self::getAttribute($chartDetail->dLbls->numFmt, 'formatCode', 'string');
1210                  $plotAttributes['numFmtLinked'] = self::getAttribute($chartDetail->dLbls->numFmt, 'sourceLinked', 'boolean');
1211              }
1212              if (isset($chartDetail->dLbls->showLegendKey)) {
1213                  $plotAttributes['showLegendKey'] = self::getAttribute($chartDetail->dLbls->showLegendKey, 'val', 'string');
1214              }
1215              if (isset($chartDetail->dLbls->showVal)) {
1216                  $plotAttributes['showVal'] = self::getAttribute($chartDetail->dLbls->showVal, 'val', 'string');
1217              }
1218              if (isset($chartDetail->dLbls->showCatName)) {
1219                  $plotAttributes['showCatName'] = self::getAttribute($chartDetail->dLbls->showCatName, 'val', 'string');
1220              }
1221              if (isset($chartDetail->dLbls->showSerName)) {
1222                  $plotAttributes['showSerName'] = self::getAttribute($chartDetail->dLbls->showSerName, 'val', 'string');
1223              }
1224              if (isset($chartDetail->dLbls->showPercent)) {
1225                  $plotAttributes['showPercent'] = self::getAttribute($chartDetail->dLbls->showPercent, 'val', 'string');
1226              }
1227              if (isset($chartDetail->dLbls->showBubbleSize)) {
1228                  $plotAttributes['showBubbleSize'] = self::getAttribute($chartDetail->dLbls->showBubbleSize, 'val', 'string');
1229              }
1230              if (isset($chartDetail->dLbls->showLeaderLines)) {
1231                  $plotAttributes['showLeaderLines'] = self::getAttribute($chartDetail->dLbls->showLeaderLines, 'val', 'string');
1232              }
1233              if (isset($chartDetail->dLbls->spPr)) {
1234                  $sppr = $chartDetail->dLbls->spPr->children($this->aNamespace);
1235                  if (isset($sppr->solidFill)) {
1236                      $plotAttributes['labelFillColor'] = new ChartColor($this->readColor($sppr->solidFill));
1237                  }
1238                  if (isset($sppr->ln->solidFill)) {
1239                      $plotAttributes['labelBorderColor'] = new ChartColor($this->readColor($sppr->ln->solidFill));
1240                  }
1241              }
1242              if (isset($chartDetail->dLbls->txPr)) {
1243                  $txpr = $chartDetail->dLbls->txPr->children($this->aNamespace);
1244                  if (isset($txpr->p)) {
1245                      $plotAttributes['labelFont'] = $this->parseFont($txpr->p);
1246                      if (isset($txpr->p->pPr->defRPr->effectLst)) {
1247                          $labelEffects = new GridLines();
1248                          $this->readEffects($txpr->p->pPr->defRPr, $labelEffects, false);
1249                          $plotAttributes['labelEffects'] = $labelEffects;
1250                      }
1251                  }
1252              }
1253          }
1254  
1255          return $plotAttributes;
1256      }
1257  
1258      /**
1259       * @param mixed $plotAttributes
1260       */
1261      private function setChartAttributes(Layout $plotArea, $plotAttributes): void
1262      {
1263          foreach ($plotAttributes as $plotAttributeKey => $plotAttributeValue) {
1264              switch ($plotAttributeKey) {
1265                  case 'showLegendKey':
1266                      $plotArea->setShowLegendKey($plotAttributeValue);
1267  
1268                      break;
1269                  case 'showVal':
1270                      $plotArea->setShowVal($plotAttributeValue);
1271  
1272                      break;
1273                  case 'showCatName':
1274                      $plotArea->setShowCatName($plotAttributeValue);
1275  
1276                      break;
1277                  case 'showSerName':
1278                      $plotArea->setShowSerName($plotAttributeValue);
1279  
1280                      break;
1281                  case 'showPercent':
1282                      $plotArea->setShowPercent($plotAttributeValue);
1283  
1284                      break;
1285                  case 'showBubbleSize':
1286                      $plotArea->setShowBubbleSize($plotAttributeValue);
1287  
1288                      break;
1289                  case 'showLeaderLines':
1290                      $plotArea->setShowLeaderLines($plotAttributeValue);
1291  
1292                      break;
1293              }
1294          }
1295      }
1296  
1297      private function readEffects(SimpleXMLElement $chartDetail, ?ChartProperties $chartObject, bool $getSppr = true): void
1298      {
1299          if (!isset($chartObject)) {
1300              return;
1301          }
1302          if ($getSppr) {
1303              if (!isset($chartDetail->spPr)) {
1304                  return;
1305              }
1306              $sppr = $chartDetail->spPr->children($this->aNamespace);
1307          } else {
1308              $sppr = $chartDetail;
1309          }
1310          if (isset($sppr->effectLst->glow)) {
1311              $axisGlowSize = (float) self::getAttribute($sppr->effectLst->glow, 'rad', 'integer') / ChartProperties::POINTS_WIDTH_MULTIPLIER;
1312              if ($axisGlowSize != 0.0) {
1313                  $colorArray = $this->readColor($sppr->effectLst->glow);
1314                  $chartObject->setGlowProperties($axisGlowSize, $colorArray['value'], $colorArray['alpha'], $colorArray['type']);
1315              }
1316          }
1317  
1318          if (isset($sppr->effectLst->softEdge)) {
1319              /** @var string */
1320              $softEdgeSize = self::getAttribute($sppr->effectLst->softEdge, 'rad', 'string');
1321              if (is_numeric($softEdgeSize)) {
1322                  $chartObject->setSoftEdges((float) ChartProperties::xmlToPoints($softEdgeSize));
1323              }
1324          }
1325  
1326          $type = '';
1327          foreach (self::SHADOW_TYPES as $shadowType) {
1328              if (isset($sppr->effectLst->$shadowType)) {
1329                  $type = $shadowType;
1330  
1331                  break;
1332              }
1333          }
1334          if ($type !== '') {
1335              /** @var string */
1336              $blur = self::getAttribute($sppr->effectLst->$type, 'blurRad', 'string');
1337              $blur = is_numeric($blur) ? ChartProperties::xmlToPoints($blur) : null;
1338              /** @var string */
1339              $dist = self::getAttribute($sppr->effectLst->$type, 'dist', 'string');
1340              $dist = is_numeric($dist) ? ChartProperties::xmlToPoints($dist) : null;
1341              /** @var string */
1342              $direction = self::getAttribute($sppr->effectLst->$type, 'dir', 'string');
1343              $direction = is_numeric($direction) ? ChartProperties::xmlToAngle($direction) : null;
1344              $algn = self::getAttribute($sppr->effectLst->$type, 'algn', 'string');
1345              $rot = self::getAttribute($sppr->effectLst->$type, 'rotWithShape', 'string');
1346              $size = [];
1347              foreach (['sx', 'sy'] as $sizeType) {
1348                  $sizeValue = self::getAttribute($sppr->effectLst->$type, $sizeType, 'string');
1349                  if (is_numeric($sizeValue)) {
1350                      $size[$sizeType] = ChartProperties::xmlToTenthOfPercent((string) $sizeValue);
1351                  } else {
1352                      $size[$sizeType] = null;
1353                  }
1354              }
1355              foreach (['kx', 'ky'] as $sizeType) {
1356                  $sizeValue = self::getAttribute($sppr->effectLst->$type, $sizeType, 'string');
1357                  if (is_numeric($sizeValue)) {
1358                      $size[$sizeType] = ChartProperties::xmlToAngle((string) $sizeValue);
1359                  } else {
1360                      $size[$sizeType] = null;
1361                  }
1362              }
1363              $colorArray = $this->readColor($sppr->effectLst->$type);
1364              $chartObject
1365                  ->setShadowProperty('effect', $type)
1366                  ->setShadowProperty('blur', $blur)
1367                  ->setShadowProperty('direction', $direction)
1368                  ->setShadowProperty('distance', $dist)
1369                  ->setShadowProperty('algn', $algn)
1370                  ->setShadowProperty('rotWithShape', $rot)
1371                  ->setShadowProperty('size', $size)
1372                  ->setShadowProperty('color', $colorArray);
1373          }
1374      }
1375  
1376      private const SHADOW_TYPES = [
1377          'outerShdw',
1378          'innerShdw',
1379      ];
1380  
1381      private function readColor(SimpleXMLElement $colorXml): array
1382      {
1383          $result = [
1384              'type' => null,
1385              'value' => null,
1386              'alpha' => null,
1387              'brightness' => null,
1388          ];
1389          foreach (ChartColor::EXCEL_COLOR_TYPES as $type) {
1390              if (isset($colorXml->$type)) {
1391                  $result['type'] = $type;
1392                  $result['value'] = self::getAttribute($colorXml->$type, 'val', 'string');
1393                  if (isset($colorXml->$type->alpha)) {
1394                      /** @var string */
1395                      $alpha = self::getAttribute($colorXml->$type->alpha, 'val', 'string');
1396                      if (is_numeric($alpha)) {
1397                          $result['alpha'] = ChartColor::alphaFromXml($alpha);
1398                      }
1399                  }
1400                  if (isset($colorXml->$type->lumMod)) {
1401                      /** @var string */
1402                      $brightness = self::getAttribute($colorXml->$type->lumMod, 'val', 'string');
1403                      if (is_numeric($brightness)) {
1404                          $result['brightness'] = ChartColor::alphaFromXml($brightness);
1405                      }
1406                  }
1407  
1408                  break;
1409              }
1410          }
1411  
1412          return $result;
1413      }
1414  
1415      private function readLineStyle(SimpleXMLElement $chartDetail, ?ChartProperties $chartObject): void
1416      {
1417          if (!isset($chartObject, $chartDetail->spPr)) {
1418              return;
1419          }
1420          $sppr = $chartDetail->spPr->children($this->aNamespace);
1421  
1422          if (!isset($sppr->ln)) {
1423              return;
1424          }
1425          $lineWidth = null;
1426          /** @var string */
1427          $lineWidthTemp = self::getAttribute($sppr->ln, 'w', 'string');
1428          if (is_numeric($lineWidthTemp)) {
1429              $lineWidth = ChartProperties::xmlToPoints($lineWidthTemp);
1430          }
1431          /** @var string */
1432          $compoundType = self::getAttribute($sppr->ln, 'cmpd', 'string');
1433          /** @var string */
1434          $dashType = self::getAttribute($sppr->ln->prstDash, 'val', 'string');
1435          /** @var string */
1436          $capType = self::getAttribute($sppr->ln, 'cap', 'string');
1437          if (isset($sppr->ln->miter)) {
1438              $joinType = ChartProperties::LINE_STYLE_JOIN_MITER;
1439          } elseif (isset($sppr->ln->bevel)) {
1440              $joinType = ChartProperties::LINE_STYLE_JOIN_BEVEL;
1441          } else {
1442              $joinType = '';
1443          }
1444          $headArrowSize = '';
1445          $endArrowSize = '';
1446          /** @var string */
1447          $headArrowType = self::getAttribute($sppr->ln->headEnd, 'type', 'string');
1448          /** @var string */
1449          $headArrowWidth = self::getAttribute($sppr->ln->headEnd, 'w', 'string');
1450          /** @var string */
1451          $headArrowLength = self::getAttribute($sppr->ln->headEnd, 'len', 'string');
1452          /** @var string */
1453          $endArrowType = self::getAttribute($sppr->ln->tailEnd, 'type', 'string');
1454          /** @var string */
1455          $endArrowWidth = self::getAttribute($sppr->ln->tailEnd, 'w', 'string');
1456          /** @var string */
1457          $endArrowLength = self::getAttribute($sppr->ln->tailEnd, 'len', 'string');
1458          $chartObject->setLineStyleProperties(
1459              $lineWidth,
1460              $compoundType,
1461              $dashType,
1462              $capType,
1463              $joinType,
1464              $headArrowType,
1465              $headArrowSize,
1466              $endArrowType,
1467              $endArrowSize,
1468              $headArrowWidth,
1469              $headArrowLength,
1470              $endArrowWidth,
1471              $endArrowLength
1472          );
1473          $colorArray = $this->readColor($sppr->ln->solidFill);
1474          $chartObject->getLineColor()->setColorPropertiesArray($colorArray);
1475      }
1476  
1477      private function setAxisProperties(SimpleXMLElement $chartDetail, ?Axis $whichAxis): void
1478      {
1479          if (!isset($whichAxis)) {
1480              return;
1481          }
1482          if (isset($chartDetail->delete)) {
1483              $whichAxis->setAxisOption('hidden', (string) self::getAttribute($chartDetail->delete, 'val', 'string'));
1484          }
1485          if (isset($chartDetail->numFmt)) {
1486              $whichAxis->setAxisNumberProperties(
1487                  (string) self::getAttribute($chartDetail->numFmt, 'formatCode', 'string'),
1488                  null,
1489                  (int) self::getAttribute($chartDetail->numFmt, 'sourceLinked', 'int')
1490              );
1491          }
1492          if (isset($chartDetail->crossBetween)) {
1493              $whichAxis->setCrossBetween((string) self::getAttribute($chartDetail->crossBetween, 'val', 'string'));
1494          }
1495          if (isset($chartDetail->majorTickMark)) {
1496              $whichAxis->setAxisOption('major_tick_mark', (string) self::getAttribute($chartDetail->majorTickMark, 'val', 'string'));
1497          }
1498          if (isset($chartDetail->minorTickMark)) {
1499              $whichAxis->setAxisOption('minor_tick_mark', (string) self::getAttribute($chartDetail->minorTickMark, 'val', 'string'));
1500          }
1501          if (isset($chartDetail->tickLblPos)) {
1502              $whichAxis->setAxisOption('axis_labels', (string) self::getAttribute($chartDetail->tickLblPos, 'val', 'string'));
1503          }
1504          if (isset($chartDetail->crosses)) {
1505              $whichAxis->setAxisOption('horizontal_crosses', (string) self::getAttribute($chartDetail->crosses, 'val', 'string'));
1506          }
1507          if (isset($chartDetail->crossesAt)) {
1508              $whichAxis->setAxisOption('horizontal_crosses_value', (string) self::getAttribute($chartDetail->crossesAt, 'val', 'string'));
1509          }
1510          if (isset($chartDetail->scaling->orientation)) {
1511              $whichAxis->setAxisOption('orientation', (string) self::getAttribute($chartDetail->scaling->orientation, 'val', 'string'));
1512          }
1513          if (isset($chartDetail->scaling->max)) {
1514              $whichAxis->setAxisOption('maximum', (string) self::getAttribute($chartDetail->scaling->max, 'val', 'string'));
1515          }
1516          if (isset($chartDetail->scaling->min)) {
1517              $whichAxis->setAxisOption('minimum', (string) self::getAttribute($chartDetail->scaling->min, 'val', 'string'));
1518          }
1519          if (isset($chartDetail->scaling->min)) {
1520              $whichAxis->setAxisOption('minimum', (string) self::getAttribute($chartDetail->scaling->min, 'val', 'string'));
1521          }
1522          if (isset($chartDetail->majorUnit)) {
1523              $whichAxis->setAxisOption('major_unit', (string) self::getAttribute($chartDetail->majorUnit, 'val', 'string'));
1524          }
1525          if (isset($chartDetail->minorUnit)) {
1526              $whichAxis->setAxisOption('minor_unit', (string) self::getAttribute($chartDetail->minorUnit, 'val', 'string'));
1527          }
1528          if (isset($chartDetail->baseTimeUnit)) {
1529              $whichAxis->setAxisOption('baseTimeUnit', (string) self::getAttribute($chartDetail->baseTimeUnit, 'val', 'string'));
1530          }
1531          if (isset($chartDetail->majorTimeUnit)) {
1532              $whichAxis->setAxisOption('majorTimeUnit', (string) self::getAttribute($chartDetail->majorTimeUnit, 'val', 'string'));
1533          }
1534          if (isset($chartDetail->minorTimeUnit)) {
1535              $whichAxis->setAxisOption('minorTimeUnit', (string) self::getAttribute($chartDetail->minorTimeUnit, 'val', 'string'));
1536          }
1537          if (isset($chartDetail->txPr)) {
1538              $children = $chartDetail->txPr->children($this->aNamespace);
1539              $addAxisText = false;
1540              $axisText = new AxisText();
1541              if (isset($children->bodyPr)) {
1542                  /** @var string */
1543                  $textRotation = self::getAttribute($children->bodyPr, 'rot', 'string');
1544                  if (is_numeric($textRotation)) {
1545                      $axisText->setRotation((int) ChartProperties::xmlToAngle($textRotation));
1546                      $addAxisText = true;
1547                  }
1548              }
1549              if (isset($children->p->pPr->defRPr)) {
1550                  $font = $this->parseFont($children->p);
1551                  if ($font !== null) {
1552                      $axisText->setFont($font);
1553                      $addAxisText = true;
1554                  }
1555              }
1556              if (isset($children->p->pPr->defRPr->effectLst)) {
1557                  $this->readEffects($children->p->pPr->defRPr, $axisText, false);
1558                  $addAxisText = true;
1559              }
1560              if ($addAxisText) {
1561                  $whichAxis->setAxisText($axisText);
1562              }
1563          }
1564      }
1565  }