From 8bde1ace4488462ee8647d37d749da1fbd9edecf Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 6 Aug 2022 18:06:36 -0700 Subject: [PATCH] Charts Support for Rounded Corners and Trendlines (#2976) Fix #2968. Fix #2815. Solution largely based on suggestions by @bridgeplayr. --- .../33_Chart_create_scatter5_trendlines.php | 273 ++++++++++++++++++ samples/templates/32readwriteAreaChart1.xlsx | Bin 12588 -> 13641 bytes .../32readwriteScatterChartTrendlines1.xlsx | Bin 0 -> 15275 bytes src/PhpSpreadsheet/Chart/Chart.php | 15 + src/PhpSpreadsheet/Chart/DataSeriesValues.php | 15 + src/PhpSpreadsheet/Chart/TrendLine.php | 126 ++++++++ src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 37 +++ src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 62 +++- .../Chart/RoundedCornersTest.php | 74 +++++ .../Chart/TrendLineTest.php | 97 +++++++ 10 files changed, 698 insertions(+), 1 deletion(-) create mode 100644 samples/Chart/33_Chart_create_scatter5_trendlines.php create mode 100644 samples/templates/32readwriteScatterChartTrendlines1.xlsx create mode 100644 src/PhpSpreadsheet/Chart/TrendLine.php create mode 100644 tests/PhpSpreadsheetTests/Chart/RoundedCornersTest.php create mode 100644 tests/PhpSpreadsheetTests/Chart/TrendLineTest.php diff --git a/samples/Chart/33_Chart_create_scatter5_trendlines.php b/samples/Chart/33_Chart_create_scatter5_trendlines.php new file mode 100644 index 00000000..a87f6ee1 --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter5_trendlines.php @@ -0,0 +1,273 @@ +getActiveSheet(); +$dataSheet->setTitle('Data'); +// changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date +$dataSheet->fromArray( + [ + ['', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE("2021-01-01")', 12.1, 15.1, 21.1], + ['=DATEVALUE("2021-04-01")', 56.2, 73.2, 86.2], + ['=DATEVALUE("2021-07-01")', 52.2, 61.2, 69.2], + ['=DATEVALUE("2021-10-01")', 30.2, 22.2, 0.2], + ['=DATEVALUE("2022-01-01")', 40.1, 38.1, 65.1], + ['=DATEVALUE("2022-04-01")', 45.2, 44.2, 96.2], + ['=DATEVALUE("2022-07-01")', 52.2, 51.2, 55.2], + ['=DATEVALUE("2022-10-01")', 41.2, 72.2, 56.2], + ] +); + +$dataSheet->getStyle('A2:A9')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); +$dataSheet->getColumnDimension('A')->setAutoSize(true); +$dataSheet->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 +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$B$1', null, 1), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$C$1', null, 1), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$D$1', null, 1), +]; +// Set the X-Axis Labels +// NUMBER, not STRING +// added x-axis values for each of the 3 metrics +// added FORMATE_CODE_NUMBER +// Number of datapoints in series +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), +]; +// 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 +// Data Marker Color fill/[fill,Border] +// Data Marker size +// Color(s) added +// added FORMAT_CODE_NUMBER +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$B$2:$B$9', Properties::FORMAT_CODE_NUMBER, 8, null, 'diamond', null, 5), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$C$2:$C$9', Properties::FORMAT_CODE_NUMBER, 8, null, 'square', '*accent1', 6), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$D$2:$D$9', Properties::FORMAT_CODE_NUMBER, 8, null, null, null, 7), // let Excel choose marker shape +]; +// series 1 - metric1 +// marker details +$dataSeriesValues[0] + ->getMarkerFillColor() + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + +// line details - dashed, smooth line (Bezier) with arrows, 40% transparent +$dataSeriesValues[0] + ->setSmoothLine(true) + ->setScatterLines(true) + ->setLineColorProperties('accent1', 40, ChartColor::EXCEL_COLOR_TYPE_SCHEME); // value, alpha, type +$dataSeriesValues[0]->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_TRIPLE, // compound + Properties::LINE_STYLE_DASH_SQUARE_DOT, // dash + Properties::LINE_STYLE_CAP_SQUARE, // cap + Properties::LINE_STYLE_JOIN_MITER, // join + Properties::LINE_STYLE_ARROW_TYPE_OPEN, // head type + Properties::LINE_STYLE_ARROW_SIZE_4, // head size preset index + Properties::LINE_STYLE_ARROW_TYPE_ARROW, // end type + Properties::LINE_STYLE_ARROW_SIZE_6 // end size preset index +); + +// series 2 - metric2, straight line - no special effects, connected +$dataSeriesValues[1] // square marker border color + ->getMarkerBorderColor() + ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[1] // square marker fill color + ->getMarkerFillColor() + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[1] + ->setScatterLines(true) + ->setSmoothLine(false) + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[1]->setLineWidth(2.0); + +// series 3 - metric3, markers, no line +$dataSeriesValues[2] // triangle? fill + //->setPointMarker('triangle') // let Excel choose shape, which is predicted to be a triangle + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[2] // triangle border + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[2]->setScatterLines(false); // points not connected + // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +$xAxis = new Axis(); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + null, // smooth line + DataSeries::STYLE_SMOOTHMARKER // 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 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 + // $yAxis, // yAxis +); + +// Set the position of the chart in the chart sheet +$chart->setTopLeftPosition('A1'); +$chart->setBottomRightPosition('P12'); + +// create a 'Chart' worksheet, add $chart to it +$spreadsheet->createSheet(); +$chartSheet = $spreadsheet->getSheet(1); +$chartSheet->setTitle('Scatter Chart'); + +$chartSheet = $spreadsheet->getSheetByName('Scatter Chart'); +// Add the chart to the worksheet +$chartSheet->addChart($chart); + +// ------------ Demonstrate Trendlines for metric3 values in a new chart ------------ + +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$D$1', null, 1), +]; +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), +]; + +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$D$2:$D$9', Properties::FORMAT_CODE_NUMBER, 4, null, 'triangle', null, 7), +]; + +// add 3 trendlines: +// 1- linear, double-ended arrow, w=0.5, same color as marker fill; nodispRSqr, nodispEq +// 2- polynomial (order=3) no-arrow trendline, w=1.25, same color as marker fill; dispRSqr, dispEq +// 3- moving Avg (period=2) single-arrow trendline, w=1.5, same color as marker fill; no dispRSqr, no dispEq +$trendLines = [ + new TrendLine(TrendLine::TRENDLINE_LINEAR, null, null, false, false), + new TrendLine(TrendLine::TRENDLINE_POLYNOMIAL, 3, null, true, true), + new TrendLine(TrendLine::TRENDLINE_MOVING_AVG, null, 2, true), +]; +$dataSeriesValues[0]->setTrendLines($trendLines); + +// Suppress connecting lines; instead, add different Trendline algorithms to +// determine how well the data fits the algorithm (Rsquared="goodness of fit") +// Display RSqr plus the eqn just because we can. + +$dataSeriesValues[0]->setScatterLines(false); // points not connected +$dataSeriesValues[0]->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[0]->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); + +// add properties to the trendLines - give each a different color +$dataSeriesValues[0]->getTrendLines()[0]->getLineColor()->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[0]->getTrendLines()[0]->setLineStyleProperties(0.5, null, null, null, null, Properties::LINE_STYLE_ARROW_TYPE_STEALTH, 5, Properties::LINE_STYLE_ARROW_TYPE_OPEN, 8); + +$dataSeriesValues[0]->getTrendLines()[1]->getLineColor()->setColorProperties('accent3', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[0]->getTrendLines()[1]->setLineStyleProperties(1.25); + +$dataSeriesValues[0]->getTrendLines()[2]->getLineColor()->setColorProperties('accent2', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[0]->getTrendLines()[2]->setLineStyleProperties(1.5, null, null, null, null, null, null, Properties::LINE_STYLE_ARROW_TYPE_OPEN, 8); + +$xAxis = new Axis(); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); // m/d/yyyy + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + null, // smooth line + DataSeries::STYLE_SMOOTHMARKER // 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 Chart - trendlines for metric3 values'); +$yAxisLabel = new Title('Value ($k)'); + +// Create the chart +$chart = new Chart( + 'chart2', // 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 + // $yAxis, // yAxis +); + +// Set the position of the chart in the chart sheet below the first chart +$chart->setTopLeftPosition('A13'); +$chart->setBottomRightPosition('P25'); + +// Add the chart to the worksheet $chartSheet +$chartSheet->addChart($chart); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/templates/32readwriteAreaChart1.xlsx b/samples/templates/32readwriteAreaChart1.xlsx index d44ae5340c3f7f1f252f4d891fd525427876ff71..474affbe8cf0f731f8fcc25c306d697aad59fb86 100644 GIT binary patch delta 9985 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 diff --git a/samples/templates/32readwriteScatterChartTrendlines1.xlsx b/samples/templates/32readwriteScatterChartTrendlines1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f48366fe0a3705bfaa1c91671a26c72b054b72e1 GIT binary patch literal 15275 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 S56XuEumcXdl5_m=?*9RVK&c7< literal 0 HcmV?d00001 diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index e850f502..556b0eff 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -147,6 +147,9 @@ class Chart /** @var bool */ private $noFill = false; + /** @var bool */ + private $roundedCorners = false; + /** * Create a new Chart. * majorGridlines and minorGridlines are deprecated, moved to Axis. @@ -762,4 +765,16 @@ class Chart return $this; } + + public function getRoundedCorners(): bool + { + return $this->roundedCorners; + } + + public function setRoundedCorners(bool $roundedCorners): self + { + $this->roundedCorners = $roundedCorners; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index cb5fa742..7d29e9c4 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -88,6 +88,9 @@ class DataSeriesValues extends Properties /** @var ?Layout */ private $labelLayout; + /** @var TrendLine[] */ + private $trendLines = []; + /** * Create a new DataSeriesValues object. * @@ -577,4 +580,16 @@ class DataSeriesValues extends Properties return $this; } + + public function setTrendLines(array $trendLines): self + { + $this->trendLines = $trendLines; + + return $this; + } + + public function getTrendLines(): array + { + return $this->trendLines; + } } diff --git a/src/PhpSpreadsheet/Chart/TrendLine.php b/src/PhpSpreadsheet/Chart/TrendLine.php new file mode 100644 index 00000000..e177f819 --- /dev/null +++ b/src/PhpSpreadsheet/Chart/TrendLine.php @@ -0,0 +1,126 @@ +setTrendLineProperties($trendLineType, $order, $period, $dispRSqr, $dispEq); + } + + public function getTrendLineType(): string + { + return $this->trendLineType; + } + + public function setTrendLineType(string $trendLineType): self + { + $this->trendLineType = $trendLineType; + + return $this; + } + + public function getOrder(): int + { + return $this->order; + } + + public function setOrder(int $order): self + { + $this->order = $order; + + return $this; + } + + public function getPeriod(): int + { + return $this->period; + } + + public function setPeriod(int $period): self + { + $this->period = $period; + + return $this; + } + + public function getDispRSqr(): bool + { + return $this->dispRSqr; + } + + public function setDispRSqr(bool $dispRSqr): self + { + $this->dispRSqr = $dispRSqr; + + return $this; + } + + public function getDispEq(): bool + { + return $this->dispEq; + } + + public function setDispEq(bool $dispEq): self + { + $this->dispEq = $dispEq; + + return $this; + } + + public function setTrendLineProperties(?string $trendLineType = null, ?int $order = 0, ?int $period = 0, ?bool $dispRSqr = false, ?bool $dispEq = false): self + { + if (!empty($trendLineType)) { + $this->setTrendLineType($trendLineType); + } + if ($order !== null) { + $this->setOrder($order); + } + if ($period !== null) { + $this->setPeriod($period); + } + if ($dispRSqr !== null) { + $this->setDispRSqr($dispRSqr); + } + if ($dispEq !== null) { + $this->setDispEq($dispEq); + } + + return $this; + } +} diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index e7314d0b..dab2b410 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -13,6 +13,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; +use PhpOffice\PhpSpreadsheet\Chart\TrendLine; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Style\Font; use SimpleXMLElement; @@ -76,6 +77,7 @@ class Chart $chartNoFill = false; $gradientArray = []; $gradientLin = null; + $roundedCorners = false; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { case 'spPr': @@ -84,6 +86,11 @@ class Chart $chartNoFill = true; } + break; + case 'roundedCorners': + /** @var bool */ + $roundedCorners = self::getAttribute($chartElementsC->roundedCorners, 'val', 'boolean'); + break; case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { @@ -370,6 +377,7 @@ class Chart if ($chartNoFill) { $chart->setNoFill(true); } + $chart->setRoundedCorners($roundedCorners); if (is_bool($autoTitleDeleted)) { $chart->setAutoTitleDeleted($autoTitleDeleted); } @@ -466,6 +474,7 @@ class Chart $markerBorderColor = null; $lineStyle = null; $labelLayout = null; + $trendLines = []; foreach ($seriesDetails as $seriesKey => $seriesDetail) { switch ($seriesKey) { case 'idx': @@ -513,6 +522,23 @@ class Chart } } + break; + case 'trendline': + $trendLine = new TrendLine(); + $this->readLineStyle($seriesDetail, $trendLine); + /** @var ?string */ + $trendLineType = self::getAttribute($seriesDetail->trendlineType, 'val', 'string'); + /** @var ?bool */ + $dispRSqr = self::getAttribute($seriesDetail->dispRSqr, 'val', 'boolean'); + /** @var ?bool */ + $dispEq = self::getAttribute($seriesDetail->dispEq, 'val', 'boolean'); + /** @var ?int */ + $order = self::getAttribute($seriesDetail->order, 'val', 'integer'); + /** @var ?int */ + $period = self::getAttribute($seriesDetail->period, 'val', 'integer'); + $trendLine->setTrendLineProperties($trendLineType, $order, $period, $dispRSqr, $dispEq); + $trendLines[] = $trendLine; + break; case 'marker': $marker = self::getAttribute($seriesDetail->symbol, 'val', 'string'); @@ -651,6 +677,17 @@ class Chart $seriesValues[$seriesIndex]->setSmoothLine(true); } } + if (!empty($trendLines)) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setTrendLines($trendLines); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setTrendLines($trendLines); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setTrendLines($trendLines); + } + } } } /** @phpstan-ignore-next-line */ diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 278b64e7..ad746a0a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -11,6 +11,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; +use PhpOffice\PhpSpreadsheet\Chart\TrendLine; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; @@ -58,7 +59,7 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', 'en-GB'); $objWriter->endElement(); $objWriter->startElement('c:roundedCorners'); - $objWriter->writeAttribute('val', '0'); + $objWriter->writeAttribute('val', $chart->getRoundedCorners() ? '1' : '0'); $objWriter->endElement(); $this->writeAlternateContent($objWriter); @@ -1123,6 +1124,65 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } + // Trendlines + if ($plotSeriesValues !== false) { + foreach ($plotSeriesValues->getTrendLines() as $trendLine) { + $trendLineType = $trendLine->getTrendLineType(); + $order = $trendLine->getOrder(); + $period = $trendLine->getPeriod(); + $dispRSqr = $trendLine->getDispRSqr(); + $dispEq = $trendLine->getDispEq(); + $trendLineColor = $trendLine->getLineColor(); // ChartColor + $trendLineWidth = $trendLine->getLineStyleProperty('width'); + + $objWriter->startElement('c:trendline'); // N.B. lowercase 'ell' + $objWriter->startElement('c:spPr'); + + if (!$trendLineColor->isUsable()) { + // use dataSeriesValues line color as a backup if $trendLineColor is null + $dsvLineColor = $plotSeriesValues->getLineColor(); + if ($dsvLineColor->isUsable()) { + $trendLine + ->getLineColor() + ->setColorProperties($dsvLineColor->getValue(), $dsvLineColor->getAlpha(), $dsvLineColor->getType()); + } + } // otherwise, hope Excel does the right thing + + $this->writeLineStyles($objWriter, $trendLine, false); // suppress noFill + + $objWriter->endElement(); // spPr + + $objWriter->startElement('c:trendlineType'); // N.B lowercase 'ell' + $objWriter->writeAttribute('val', $trendLineType); + $objWriter->endElement(); // trendlineType + if ($trendLineType == TrendLine::TRENDLINE_POLYNOMIAL) { + $objWriter->startElement('c:order'); + $objWriter->writeAttribute('val', $order); + $objWriter->endElement(); // order + } + if ($trendLineType == TrendLine::TRENDLINE_MOVING_AVG) { + $objWriter->startElement('c:period'); + $objWriter->writeAttribute('val', $period); + $objWriter->endElement(); // period + } + $objWriter->startElement('c:dispRSqr'); + $objWriter->writeAttribute('val', $dispRSqr ? '1' : '0'); + $objWriter->endElement(); + $objWriter->startElement('c:dispEq'); + $objWriter->writeAttribute('val', $dispEq ? '1' : '0'); + $objWriter->endElement(); + if ($groupType === DataSeries::TYPE_SCATTERCHART || $groupType === DataSeries::TYPE_LINECHART) { + $objWriter->startElement('c:trendlineLbl'); + $objWriter->startElement('c:numFmt'); + $objWriter->writeAttribute('formatCode', 'General'); + $objWriter->writeAttribute('sourceLinked', '0'); + $objWriter->endElement(); // numFmt + $objWriter->endElement(); // trendlineLbl + } + + $objWriter->endElement(); // trendline + } + } // Category Labels $plotSeriesCategory = $plotGroup->getPlotCategoryByIndex($plotSeriesIdx); diff --git a/tests/PhpSpreadsheetTests/Chart/RoundedCornersTest.php b/tests/PhpSpreadsheetTests/Chart/RoundedCornersTest.php new file mode 100644 index 00000000..93bbaa23 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/RoundedCornersTest.php @@ -0,0 +1,74 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testRounded(): void + { + $file = self::DIRECTORY . '32readwriteAreaChart1.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('Data', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertTrue($chart->getRoundedCorners()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testNotRounded(): void + { + $file = self::DIRECTORY . '32readwriteAreaChart2.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('Data', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertFalse($chart->getRoundedCorners()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php b/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php new file mode 100644 index 00000000..c8550d83 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php @@ -0,0 +1,97 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testTrendLine(): void + { + $file = self::DIRECTORY . '32readwriteScatterChartTrendlines1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getSheet(1); + self::assertSame(2, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getSheet(1); + self::assertSame('Scatter Chart', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(2, $charts); + + $chart = $charts[0]; + self::assertNotNull($chart); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + $plotSeriesArray = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeriesArray); + $plotSeries = $plotSeriesArray[0]; + $valuesArray = $plotSeries->getPlotValues(); + self::assertCount(3, $valuesArray); + self::assertEmpty($valuesArray[0]->getTrendLines()); + self::assertEmpty($valuesArray[1]->getTrendLines()); + self::assertEmpty($valuesArray[2]->getTrendLines()); + + $chart = $charts[1]; + self::assertNotNull($chart); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + $plotSeriesArray = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeriesArray); + $plotSeries = $plotSeriesArray[0]; + $valuesArray = $plotSeries->getPlotValues(); + self::assertCount(1, $valuesArray); + $trendLines = $valuesArray[0]->getTrendLines(); + self::assertCount(3, $trendLines); + + $trendLine = $trendLines[0]; + self::assertSame('linear', $trendLine->getTrendLineType()); + self::assertFalse($trendLine->getDispRSqr()); + self::assertFalse($trendLine->getDispEq()); + $lineColor = $trendLine->getLineColor(); + self::assertSame('accent4', $lineColor->getValue()); + self::assertSame('stealth', $trendLine->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals(0.5, $trendLine->getLineStyleProperty('width')); + + $trendLine = $trendLines[1]; + self::assertSame('poly', $trendLine->getTrendLineType()); + self::assertTrue($trendLine->getDispRSqr()); + self::assertTrue($trendLine->getDispEq()); + $lineColor = $trendLine->getLineColor(); + self::assertSame('accent3', $lineColor->getValue()); + self::assertNull($trendLine->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals(1.25, $trendLine->getLineStyleProperty('width')); + + $trendLine = $trendLines[2]; + self::assertSame('movingAvg', $trendLine->getTrendLineType()); + self::assertTrue($trendLine->getDispRSqr()); + self::assertFalse($trendLine->getDispEq()); + $lineColor = $trendLine->getLineColor(); + self::assertSame('accent2', $lineColor->getValue()); + self::assertNull($trendLine->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals(1.5, $trendLine->getLineStyleProperty('width')); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +}