diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index de63f72f..eabedcc8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4792,7 +4792,7 @@ parameters: - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 44 + count: 43 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - @@ -4982,7 +4982,7 @@ parameters: - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, string\\|null given\\.$#" - count: 5 + count: 4 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - diff --git a/samples/Chart/33_Chart_create_scatter2.php b/samples/Chart/33_Chart_create_scatter2.php new file mode 100644 index 00000000..59d6bd3b --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter2.php @@ -0,0 +1,121 @@ +getActiveSheet(); +// changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date +$worksheet->fromArray( + [ + ['', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE("2021-01-01")', 12.1, 15.1, 21.1], + ['=DATEVALUE("2021-01-04")', 56.2, 73.2, 86.2], + ['=DATEVALUE("2021-01-07")', 52.2, 61.2, 69.2], + ['=DATEVALUE("2021-01-10")', 30.2, 32.2, 0.2], + ] +); +$worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode('yyyy-mm-dd'); +$worksheet->getColumnDimension('A')->setAutoSize(true); +$worksheet->setSelectedCells('A1'); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // was 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // was 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // was 2012 +]; +// Set the X-Axis Labels +// changed from STRING to NUMBER +// added 2 additional x-axis values associated with each of the 3 metrics +// added FORMATE_CODE_NUMBER +$xAxisTickValues = [ + //new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +// added FORMAT_CODE_NUMBER +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', Properties::FORMAT_CODE_NUMBER, 4), +]; + // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +$xAxis = new Axis(); +//$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); +$xAxis->setAxisNumberProperties('yyyy-mm-dd'); + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have any grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + null, // smooth line + //DataSeries::STYLE_LINEMARKER // plotStyle + DataSeries::STYLE_MARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Trend Chart'); +$yAxisLabel = new Title('Value ($k)'); + +// Create the chart +$chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis +); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('A7'); +$chart->setBottomRightPosition('N20'); +// Add the chart to the worksheet +$worksheet->addChart($chart); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$spreadsheet->disconnectWorksheets(); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/templates/32readwriteScatterChart7.xlsx b/samples/templates/32readwriteScatterChart7.xlsx new file mode 100644 index 00000000..6c01bf40 Binary files /dev/null and b/samples/templates/32readwriteScatterChart7.xlsx differ diff --git a/samples/templates/32readwriteStockChart5.xlsx b/samples/templates/32readwriteStockChart5.xlsx new file mode 100644 index 00000000..364803f2 Binary files /dev/null and b/samples/templates/32readwriteStockChart5.xlsx differ diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index a4f8f61d..cf3e0853 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -73,6 +73,9 @@ class DataSeriesValues */ private $fillColor; + /** @var string */ + private $schemeClr = ''; + /** * Line Width. * @@ -83,6 +86,9 @@ class DataSeriesValues /** @var bool */ private $scatterLines = true; + /** @var bool */ + private $bubble3D = false; + /** * Create a new DataSeriesValues object. * @@ -440,4 +446,28 @@ class DataSeriesValues return $this; } + + public function getBubble3D(): bool + { + return $this->bubble3D; + } + + public function setBubble3D(bool $bubble3D): self + { + $this->bubble3D = $bubble3D; + + return $this; + } + + public function getSchemeClr(): string + { + return $this->schemeClr; + } + + public function setSchemeClr(string $schemeClr): self + { + $this->schemeClr = $schemeClr; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 4e3cd02d..4480c6ff 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -288,6 +288,8 @@ class Chart $lineWidth = null; $pointSize = null; $noFill = false; + $schemeClr = ''; + $bubble3D = false; foreach ($seriesDetails as $seriesKey => $seriesDetail) { switch ($seriesKey) { case 'idx': @@ -304,11 +306,16 @@ class Chart break; case 'spPr': - $ln = $seriesDetail->children($namespacesChartMeta['a'])->ln; + $children = $seriesDetail->children($namespacesChartMeta['a']); + $ln = $children->ln; $lineWidth = self::getAttribute($ln, 'w', 'string'); if (is_countable($ln->noFill) && count($ln->noFill) === 1) { $noFill = true; } + $sf = $children->solidFill->schemeClr; + if ($sf) { + $schemeClr = self::getAttribute($sf, 'val', 'string'); + } break; case 'marker': @@ -342,6 +349,10 @@ class Chart case 'yVal': $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, "$marker", "$srgbClr", "$pointSize"); + break; + case 'bubble3D': + $bubble3D = self::getAttribute($seriesDetail, 'val', 'boolean'); + break; } } @@ -367,6 +378,28 @@ class Chart $seriesValues[$seriesIndex]->setLineWidth((int) $lineWidth); } } + if ($schemeClr) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setSchemeClr($schemeClr); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setSchemeClr($schemeClr); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setSchemeClr($schemeClr); + } + } + if ($bubble3D) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setBubble3D($bubble3D); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setBubble3D($bubble3D); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setBubble3D($bubble3D); + } + } } } @@ -514,6 +547,9 @@ class Chart $defaultStrikethrough = null; $defaultBaseline = null; $defaultFontName = null; + $defaultLatin = null; + $defaultEastAsian = null; + $defaultComplexScript = null; $defaultColor = null; if (isset($titleDetailPart->pPr->defRPr)) { /** @var ?int */ @@ -528,9 +564,20 @@ class Chart $defaultStrikethrough = self::getAttribute($titleDetailPart->pPr->defRPr, 'strike', 'string'); /** @var ?int */ $defaultBaseline = self::getAttribute($titleDetailPart->pPr->defRPr, 'baseline', 'integer'); + if (isset($titleDetailPart->defRPr->rFont['val'])) { + $defaultFontName = (string) $titleDetailPart->defRPr->rFont['val']; + } if (isset($titleDetailPart->pPr->defRPr->latin)) { /** @var ?string */ - $defaultFontName = self::getAttribute($titleDetailPart->pPr->defRPr->latin, 'typeface', 'string'); + $defaultLatin = self::getAttribute($titleDetailPart->pPr->defRPr->latin, 'typeface', 'string'); + } + if (isset($titleDetailPart->pPr->defRPr->ea)) { + /** @var ?string */ + $defaultEastAsian = self::getAttribute($titleDetailPart->pPr->defRPr->ea, 'typeface', 'string'); + } + if (isset($titleDetailPart->pPr->defRPr->cs)) { + /** @var ?string */ + $defaultComplexScript = self::getAttribute($titleDetailPart->pPr->defRPr->cs, 'typeface', 'string'); } if (isset($titleDetailPart->pPr->defRPr->solidFill->srgbClr)) { /** @var ?string */ @@ -538,12 +585,18 @@ class Chart } } foreach ($titleDetailPart as $titleDetailElementKey => $titleDetailElement) { - if (isset($titleDetailElement->t)) { - $objText = $value->createTextRun((string) $titleDetailElement->t); - } - if ($objText === null || $objText->getFont() === null) { + if ( + (string) $titleDetailElementKey !== 'r' + || !isset($titleDetailElement->t) + ) { continue; } + $objText = $value->createTextRun((string) $titleDetailElement->t); + if ($objText->getFont() === null) { + // @codeCoverageIgnoreStart + continue; + // @codeCoverageIgnoreEnd + } $fontSize = null; $bold = null; $italic = null; @@ -551,15 +604,29 @@ class Chart $strikethrough = null; $baseline = null; $fontName = null; + $latinName = null; + $eastAsian = null; + $complexScript = null; $fontColor = null; + $uSchemeClr = null; if (isset($titleDetailElement->rPr)) { // not used now, not sure it ever was, grandfathering if (isset($titleDetailElement->rPr->rFont['val'])) { + // @codeCoverageIgnoreStart $fontName = (string) $titleDetailElement->rPr->rFont['val']; + // @codeCoverageIgnoreEnd } if (isset($titleDetailElement->rPr->latin)) { /** @var ?string */ - $fontName = self::getAttribute($titleDetailElement->rPr->latin, 'typeface', 'string'); + $latinName = self::getAttribute($titleDetailElement->rPr->latin, 'typeface', 'string'); + } + if (isset($titleDetailElement->rPr->ea)) { + /** @var ?string */ + $eastAsian = self::getAttribute($titleDetailElement->rPr->ea, 'typeface', 'string'); + } + if (isset($titleDetailElement->rPr->cs)) { + /** @var ?string */ + $complexScript = self::getAttribute($titleDetailElement->rPr->cs, 'typeface', 'string'); } /** @var ?int */ $fontSize = self::getAttribute($titleDetailElement->rPr, 'sz', 'integer'); @@ -583,43 +650,72 @@ class Chart /** @var ?string */ $underscore = self::getAttribute($titleDetailElement->rPr, 'u', 'string'); + if (isset($titleDetailElement->rPr->uFill->solidFill->schemeClr)) { + /** @var ?string */ + $uSchemeClr = self::getAttribute($titleDetailElement->rPr->uFill->solidFill->schemeClr, 'val', 'string'); + } /** @var ?string */ - $strikethrough = self::getAttribute($titleDetailElement->rPr, 's', 'string'); + $strikethrough = self::getAttribute($titleDetailElement->rPr, 'strike', 'string'); } + $fontFound = false; + $latinName = $latinName ?? $defaultLatin; + if ($latinName !== null) { + $objText->getFont()->setLatin($latinName); + $fontFound = true; + } + $eastAsian = $eastAsian ?? $defaultEastAsian; + if ($eastAsian !== null) { + $objText->getFont()->setEastAsian($eastAsian); + $fontFound = true; + } + $complexScript = $complexScript ?? $defaultComplexScript; + if ($complexScript !== null) { + $objText->getFont()->setComplexScript($complexScript); + $fontFound = true; + } $fontName = $fontName ?? $defaultFontName; if ($fontName !== null) { + // @codeCoverageIgnoreStart $objText->getFont()->setName($fontName); + $fontFound = true; + // @codeCoverageIgnoreEnd } $fontSize = $fontSize ?? $defaultFontSize; if (is_int($fontSize)) { $objText->getFont()->setSize(floor($fontSize / 100)); + $fontFound = true; } $fontColor = $fontColor ?? $defaultColor; if ($fontColor !== null) { $objText->getFont()->setColor(new Color($fontColor)); + $fontFound = true; } $bold = $bold ?? $defaultBold; if ($bold !== null) { $objText->getFont()->setBold($bold); + $fontFound = true; } $italic = $italic ?? $defaultItalic; if ($italic !== null) { $objText->getFont()->setItalic($italic); + $fontFound = true; } $baseline = $baseline ?? $defaultBaseline; if ($baseline !== null) { + $objText->getFont()->setBaseLine($baseline); if ($baseline > 0) { $objText->getFont()->setSuperscript(true); } elseif ($baseline < 0) { $objText->getFont()->setSubscript(true); } + $fontFound = true; } $underscore = $underscore ?? $defaultUnderscore; @@ -628,18 +724,29 @@ class Chart $objText->getFont()->setUnderline(Font::UNDERLINE_SINGLE); } elseif ($underscore == 'dbl') { $objText->getFont()->setUnderline(Font::UNDERLINE_DOUBLE); + } elseif ($underscore !== '') { + $objText->getFont()->setUnderline($underscore); } else { $objText->getFont()->setUnderline(Font::UNDERLINE_NONE); } + $fontFound = true; + if ($uSchemeClr) { + $objText->getFont()->setUSchemeClr($uSchemeClr); + } } $strikethrough = $strikethrough ?? $defaultStrikethrough; if ($strikethrough !== null) { + $objText->getFont()->setStrikeType($strikethrough); if ($strikethrough == 'noStrike') { $objText->getFont()->setStrikethrough(false); } else { $objText->getFont()->setStrikethrough(true); } + $fontFound = true; + } + if ($fontFound === false) { + $objText->setFont(null); } } diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index 13fe2b67..e5b056c9 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -18,6 +18,29 @@ class Font extends Supervisor */ protected $name = 'Calibri'; + /** + * The following 6 are used only for chart titles, I think. + * + *@var string + */ + private $latin = ''; + + /** @var string */ + private $eastAsian = ''; + + /** @var string */ + private $complexScript = ''; + + /** @var int */ + private $baseLine = 0; + + /** @var string */ + private $strikeType = ''; + + /** @var string */ + private $uSchemeClr = ''; + // end of chart title items + /** * Font Size. * @@ -170,6 +193,15 @@ class Font extends Supervisor if (isset($styleArray['name'])) { $this->setName($styleArray['name']); } + if (isset($styleArray['latin'])) { + $this->setLatin($styleArray['latin']); + } + if (isset($styleArray['eastAsian'])) { + $this->setEastAsian($styleArray['eastAsian']); + } + if (isset($styleArray['complexScript'])) { + $this->setComplexScript($styleArray['complexScript']); + } if (isset($styleArray['bold'])) { $this->setBold($styleArray['bold']); } @@ -213,6 +245,33 @@ class Font extends Supervisor return $this->name; } + public function getLatin(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getLatin(); + } + + return $this->latin; + } + + public function getEastAsian(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getEastAsian(); + } + + return $this->eastAsian; + } + + public function getComplexScript(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getComplexScript(); + } + + return $this->complexScript; + } + /** * Set Name. * @@ -235,6 +294,51 @@ class Font extends Supervisor return $this; } + public function setLatin(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['latin' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->latin = $fontname; + } + + return $this; + } + + public function setEastAsian(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['eastAsian' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->eastAsian = $fontname; + } + + return $this; + } + + public function setComplexScript(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['complexScript' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->complexScript = $fontname; + } + + return $this; + } + /** * Get Size. * @@ -418,6 +522,69 @@ class Font extends Supervisor return $this; } + public function getBaseLine(): int + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getBaseLine(); + } + + return $this->baseLine; + } + + public function setBaseLine(int $baseLine): self + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['baseLine' => $baseLine]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->baseLine = $baseLine; + } + + return $this; + } + + public function getStrikeType(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getStrikeType(); + } + + return $this->strikeType; + } + + public function setStrikeType(string $strikeType): self + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['strikeType' => $strikeType]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->strikeType = $strikeType; + } + + return $this; + } + + public function getUSchemeClr(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getUSchemeClr(); + } + + return $this->uSchemeClr; + } + + public function setUSchemeClr(string $uSchemeClr): self + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['uSchemeClr' => $uSchemeClr]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->uSchemeClr = $uSchemeClr; + } + + return $this; + } + /** * Get Underline. * @@ -546,6 +713,15 @@ class Font extends Supervisor $this->underline . ($this->strikethrough ? 't' : 'f') . $this->color->getHashCode() . + '*' . + $this->latin . + '*' . + $this->eastAsian . + '*' . + $this->complexScript . + $this->strikeType . + $this->uSchemeClr . + (string) $this->baseLine . __CLASS__ ); } @@ -553,12 +729,17 @@ class Font extends Supervisor protected function exportArray1(): array { $exportedArray = []; + $this->exportArray2($exportedArray, 'baseLine', $this->getBaseLine()); $this->exportArray2($exportedArray, 'bold', $this->getBold()); $this->exportArray2($exportedArray, 'color', $this->getColor()); + $this->exportArray2($exportedArray, 'complexScript', $this->getComplexScript()); + $this->exportArray2($exportedArray, 'eastAsian', $this->getEastAsian()); $this->exportArray2($exportedArray, 'italic', $this->getItalic()); + $this->exportArray2($exportedArray, 'latin', $this->getLatin()); $this->exportArray2($exportedArray, 'name', $this->getName()); $this->exportArray2($exportedArray, 'size', $this->getSize()); $this->exportArray2($exportedArray, 'strikethrough', $this->getStrikethrough()); + $this->exportArray2($exportedArray, 'strikeType', $this->getStrikeType()); $this->exportArray2($exportedArray, 'subscript', $this->getSubscript()); $this->exportArray2($exportedArray, 'superscript', $this->getSuperscript()); $this->exportArray2($exportedArray, 'underline', $this->getUnderline()); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index f18d9216..0f65ae3b 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -9,8 +9,8 @@ use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; +use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; -use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; @@ -121,6 +121,10 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); + $objWriter->endElement(); $caption = $title->getCaption(); if ((is_array($caption)) && (count($caption) > 0)) { @@ -304,9 +308,9 @@ class Chart extends WriterPart if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) { if ($chartType === DataSeries::TYPE_BUBBLECHART) { - $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id1, $id2, $catIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); + $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id2, $id1, $catIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); } else { - $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis, ($chartType === DataSeries::TYPE_SCATTERCHART) ? 'c:valAx' : 'c:catAx'); + $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis); } $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis, $majorGridlines, $minorGridlines); @@ -367,9 +371,19 @@ class Chart extends WriterPart * @param string $id2 * @param bool $isMultiLevelSeries */ - private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis, string $element = 'c:catAx'): void + private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis): void { - $objWriter->startElement($element); + // N.B. writeCategoryAxis may be invoked with the last parameter($yAxis) using $xAxis for ScatterChart, etc + // In that case, xAxis is NOT a category. + $AxisFormat = $yAxis->getAxisNumberFormat(); + if ( + $AxisFormat === Properties::FORMAT_CODE_DATE + || $AxisFormat == Properties::FORMAT_CODE_NUMBER + ) { + $objWriter->startElement('c:valAx'); + } else { + $objWriter->startElement('c:catAx'); + } if ($id1 > 0) { $objWriter->startElement('c:axId'); @@ -403,20 +417,20 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); - $objWriter->startElement('a:r'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); + $objWriter->endElement(); $caption = $xAxisLabel->getCaption(); if (is_array($caption)) { $caption = $caption[0]; } - $objWriter->startElement('a:t'); - $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($caption)); - $objWriter->endElement(); + $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a'); $objWriter->endElement(); $objWriter->endElement(); $objWriter->endElement(); - $objWriter->endElement(); $layout = $xAxisLabel->getLayout(); $this->writeLayout($objWriter, $layout); @@ -746,18 +760,17 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); - $objWriter->startElement('a:r'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); + $objWriter->endElement(); $caption = $yAxisLabel->getCaption(); if (is_array($caption)) { $caption = $caption[0]; } + $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a'); - $objWriter->startElement('a:t'); - $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($caption)); - $objWriter->endElement(); - - $objWriter->endElement(); $objWriter->endElement(); $objWriter->endElement(); $objWriter->endElement(); @@ -1104,13 +1117,26 @@ class Chart extends WriterPart } // Formatting for the points - if (($groupType == DataSeries::TYPE_LINECHART) || ($groupType == DataSeries::TYPE_STOCKCHART || ($groupType === DataSeries::TYPE_SCATTERCHART && $plotSeriesValues !== false && !$plotSeriesValues->getScatterLines()))) { + if ( + $groupType == DataSeries::TYPE_LINECHART + || $groupType == DataSeries::TYPE_STOCKCHART + || ($groupType === DataSeries::TYPE_SCATTERCHART && $plotSeriesValues !== false && !$plotSeriesValues->getScatterLines()) + || ($plotSeriesValues !== false && $plotSeriesValues->getSchemeClr()) + ) { $plotLineWidth = 12700; if ($plotSeriesValues) { $plotLineWidth = $plotSeriesValues->getLineWidth(); } $objWriter->startElement('c:spPr'); + $schemeClr = $plotLabel ? $plotLabel->getSchemeClr() : null; + if ($schemeClr) { + $objWriter->startElement('a:solidFill'); + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', $schemeClr); + $objWriter->endElement(); + $objWriter->endElement(); + } $objWriter->startElement('a:ln'); $objWriter->writeAttribute('w', $plotLineWidth); if ($groupType == DataSeries::TYPE_STOCKCHART || $groupType === DataSeries::TYPE_SCATTERCHART) { @@ -1186,7 +1212,14 @@ class Chart extends WriterPart $objWriter->startElement('c:cat'); } - $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'str'); + // xVals (Categories) are not always 'str' + // Test X-axis Label's Datatype to decide 'str' vs 'num' + $CategoryDatatype = $plotSeriesCategory->getDataType(); + if ($CategoryDatatype == DataSeriesValues::DATASERIES_TYPE_NUMBER) { + $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'num'); + } else { + $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'str'); + } $objWriter->endElement(); } @@ -1377,7 +1410,7 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:bubble3D'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', $plotSeriesValues->getBubble3D() ? '1' : '0'); $objWriter->endElement(); } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php index 6808f33e..da7d825b 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php @@ -203,7 +203,8 @@ class StringTable extends WriterPart if (!$richText instanceof RichText) { $textRun = $richText; $richText = new RichText(); - $richText->createTextRun($textRun); + $run = $richText->createTextRun($textRun); + $run->setFont(null); } if ($prefix !== null) { @@ -241,7 +242,11 @@ class StringTable extends WriterPart } $objWriter->writeAttribute('u', $underlineType); // Strikethrough - $objWriter->writeAttribute('strike', ($element->getFont()->getStrikethrough() ? 'sngStrike' : 'noStrike')); + $objWriter->writeAttribute('strike', ($element->getFont()->getStriketype() ?: 'noStrike')); + // Superscript/subscript + if ($element->getFont()->getBaseLine()) { + $objWriter->writeAttribute('baseline', (string) $element->getFont()->getBaseLine()); + } // Color $objWriter->startElement($prefix . 'solidFill'); @@ -250,10 +255,33 @@ class StringTable extends WriterPart $objWriter->endElement(); // srgbClr $objWriter->endElement(); // solidFill + // Underscore Color + if ($element->getFont()->getUSchemeClr()) { + $objWriter->startElement($prefix . 'uFill'); + $objWriter->startElement($prefix . 'solidFill'); + $objWriter->startElement($prefix . 'schemeClr'); + $objWriter->writeAttribute('val', $element->getFont()->getUSchemeClr()); + $objWriter->endElement(); // schemeClr + $objWriter->endElement(); // solidFill + $objWriter->endElement(); // uFill + } + // fontName - $objWriter->startElement($prefix . 'latin'); - $objWriter->writeAttribute('typeface', $element->getFont()->getName()); - $objWriter->endElement(); + if ($element->getFont()->getLatin()) { + $objWriter->startElement($prefix . 'latin'); + $objWriter->writeAttribute('typeface', $element->getFont()->getLatin()); + $objWriter->endElement(); + } + if ($element->getFont()->getEastAsian()) { + $objWriter->startElement($prefix . 'ea'); + $objWriter->writeAttribute('typeface', $element->getFont()->getEastAsian()); + $objWriter->endElement(); + } + if ($element->getFont()->getComplexScript()) { + $objWriter->startElement($prefix . 'cs'); + $objWriter->writeAttribute('typeface', $element->getFont()->getComplexScript()); + $objWriter->endElement(); + } $objWriter->endElement(); } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ColoredAxisLabelTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ColoredAxisLabelTest.php new file mode 100644 index 00000000..71b8da28 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ColoredAxisLabelTest.php @@ -0,0 +1,80 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testStock5(): void + { + $file = self::DIRECTORY . '32readwriteStockChart5.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Charts', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + + $xAxisLabel = $chart->getXAxisLabel(); + $captionArray = $xAxisLabel->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('X-Axis Title in Green', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertCount(1, $elements); + $run = $elements[0]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('00B050', $font->getColor()->getRGB()); + + $yAxisLabel = $chart->getYAxisLabel(); + $captionArray = $yAxisLabel->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('Y-Axis Title in Red', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertCount(1, $elements); + $run = $elements[0]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('FF0000', $font->getColor()->getRGB()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php index dc7fbc0b..76389727 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php @@ -57,7 +57,7 @@ class Charts32ScatterTest extends AbstractFunctional self::assertInstanceOf(Run::class, $run); $font = $run->getFont(); self::assertInstanceOf(Font::class, $font); - self::assertSame('Calibri', $font->getName()); + self::assertSame('Calibri', $font->getLatin()); self::assertEquals(12, $font->getSize()); self::assertTrue($font->getBold()); self::assertFalse($font->getItalic()); @@ -127,7 +127,7 @@ class Charts32ScatterTest extends AbstractFunctional self::assertInstanceOf(Run::class, $run); $font = $run->getFont(); self::assertInstanceOf(Font::class, $font); - self::assertSame('Calibri', $font->getName()); + self::assertSame('Calibri', $font->getLatin()); self::assertEquals(12, $font->getSize()); self::assertTrue($font->getBold()); self::assertFalse($font->getItalic()); @@ -141,7 +141,7 @@ class Charts32ScatterTest extends AbstractFunctional self::assertInstanceOf(Run::class, $run); $font = $run->getFont(); self::assertInstanceOf(Font::class, $font); - self::assertSame('Courier New', $font->getName()); + self::assertSame('Courier New', $font->getLatin()); self::assertEquals(10, $font->getSize()); self::assertFalse($font->getBold()); self::assertFalse($font->getItalic()); @@ -155,7 +155,7 @@ class Charts32ScatterTest extends AbstractFunctional self::assertInstanceOf(Run::class, $run); $font = $run->getFont(); self::assertInstanceOf(Font::class, $font); - self::assertSame('Calibri', $font->getName()); + self::assertSame('Calibri', $font->getLatin()); self::assertEquals(12, $font->getSize()); self::assertTrue($font->getBold()); self::assertFalse($font->getItalic()); @@ -224,7 +224,7 @@ class Charts32ScatterTest extends AbstractFunctional self::assertInstanceOf(Run::class, $run); $font = $run->getFont(); self::assertInstanceOf(Font::class, $font); - self::assertSame('Calibri', $font->getName()); + self::assertSame('Calibri', $font->getLatin()); self::assertEquals(12, $font->getSize()); self::assertTrue($font->getBold()); self::assertFalse($font->getItalic()); @@ -258,4 +258,76 @@ class Charts32ScatterTest extends AbstractFunctional $reloadedSpreadsheet->disconnectWorksheets(); } + + public function testScatter7(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart7.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Charts', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $title = $chart->getTitle(); + $captionArray = $title->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('Latin/EA/CS Title ABCאבגDEFァ', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertGreaterThan(0, count($elements)); + foreach ($elements as $run) { + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Times New Roman', $font->getLatin()); + self::assertSame('Malgun Gothic', $font->getEastAsian()); + self::assertSame('Courier New', $font->getComplexScript()); + self::assertEquals(12, $font->getSize()); + self::assertTrue($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('none', $font->getUnderline()); + self::assertSame('000000', $font->getColor()->getRGB()); + } + + $plotArea = $chart->getPlotArea(); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(3, $plotValues); + $values = $plotValues[0]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[1]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[2]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(7, $values->getPointSize()); + self::assertSame('FFFF00', $values->getFillColor()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } }