From b8456105dd5ea44210b5f33dcac3ab732af4444d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 23 Jul 2022 08:06:13 -0700 Subject: [PATCH] Charts - Gradients, Transparency, Hidden Axes (#2950) Fix #2257. Fix #2929. Fix #2935 (probably in a way that will not satisfy the requester). 2257 and 2929 requested changes that ultimately affect the same section of code, so it's appropriate to deal with them together. 2257 requests the ability to make the chart background transparent (so that the Excel gridlines are visible beneath the chart), and the ability to hide an Axis. 2929 requests the ability to set a gradient background on the chart. --- samples/Chart/33_Chart_create_scatter3.php | 192 ++++++++++++++++++ samples/Chart/33_Chart_create_scatter4.php | 130 ++++++++++++ .../templates/32readwriteScatterChart10.xlsx | Bin 0 -> 12363 bytes .../templates/32readwriteScatterChart9.xlsx | Bin 0 -> 12554 bytes src/PhpSpreadsheet/Chart/Axis.php | 5 +- src/PhpSpreadsheet/Chart/Chart.php | 15 ++ src/PhpSpreadsheet/Chart/ChartColor.php | 32 ++- src/PhpSpreadsheet/Chart/DataSeries.php | 2 +- src/PhpSpreadsheet/Chart/DataSeriesValues.php | 2 +- src/PhpSpreadsheet/Chart/PlotArea.php | 62 ++++++ src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 51 +++++ src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 56 ++++- .../Chart/Charts32ScatterTest.php | 85 ++++++++ 13 files changed, 619 insertions(+), 13 deletions(-) create mode 100644 samples/Chart/33_Chart_create_scatter3.php create mode 100644 samples/Chart/33_Chart_create_scatter4.php create mode 100644 samples/templates/32readwriteScatterChart10.xlsx create mode 100644 samples/templates/32readwriteScatterChart9.xlsx diff --git a/samples/Chart/33_Chart_create_scatter3.php b/samples/Chart/33_Chart_create_scatter3.php new file mode 100644 index 0000000000..b4fc97b19b --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter3.php @@ -0,0 +1,192 @@ +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(Properties::FORMAT_CODE_DATE_ISO8601); +$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), +]; + +// series 1 +// marker details +$dataSeriesValues[0] + ->setPointMarker('diamond') + ->setPointSize(5) + ->getMarkerFillColor() + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_RGB); + +// line details - smooth line, connected +$dataSeriesValues[0] + ->setScatterLines(true) + ->setSmoothLine(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 - straight line - no special effects, connected, straight line +$dataSeriesValues[1] // square fill + ->setPointMarker('square') + ->setPointSize(6) + ->getMarkerBorderColor() + ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[1] // square border + ->getMarkerFillColor() + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1] + ->setScatterLines(true) + ->setSmoothLine(false) + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1]->setLineWidth(2.0); + +// series 3 - markers, no line +$dataSeriesValues[2] // triangle fill + //->setPointMarker('triangle') // let Excel choose shape + ->setPointSize(7) + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$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 ); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); +$xAxis->setAxisOption('textRotation', '45'); +$xAxis->setAxisOption('hidden', '1'); + +$yAxis = new Axis(); +$yAxis->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_SIMPLE, + Properties::LINE_STYLE_DASH_DASH_DOT, + Properties::LINE_STYLE_CAP_FLAT, + Properties::LINE_STYLE_JOIN_BEVEL +); +$yAxis->setLineColorProperties('ffc000', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$yAxis->setAxisOption('hidden', '1'); + +// 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 + false, // smooth line + DataSeries::STYLE_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +$plotArea->setNoFill(true); +// 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 + null, //$yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + $yAxis, // yAxis +); +$chart->setNoFill(true); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('A7'); +$chart->setBottomRightPosition('P20'); +// 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/Chart/33_Chart_create_scatter4.php b/samples/Chart/33_Chart_create_scatter4.php new file mode 100644 index 0000000000..d42933b69e --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter4.php @@ -0,0 +1,130 @@ +getActiveSheet(); +$worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] +); + +// 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), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 +]; +// Set the X-Axis Labels +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 +]; +// 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 +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), +]; + +// 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 +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); + +$pos1 = 0; // pos = 0% (extreme low side or lower left corner) +$brightness1 = 0; // 0% +$gsColor1 = new ChartColor(); +$gsColor1->setColorProperties('FF0000', 75, 'srgbClr', $brightness1); // red +$gradientStop1 = [$pos1, $gsColor1]; + +$pos2 = 0.5; // pos = 50% (middle) +$brightness2 = 0.5; // 50% +$gsColor2 = new ChartColor(); +$gsColor2->setColorProperties('FFFF00', 50, 'srgbClr', $brightness2); // yellow +$gradientStop2 = [$pos2, $gsColor2]; + +$pos3 = 1.0; // pos = 100% (extreme high side or upper right corner) +$brightness3 = 0.5; // 50% +$gsColor3 = new ChartColor(); +$gsColor3->setColorProperties('00B050', 50, 'srgbClr', $brightness3); // green +$gradientStop3 = [$pos3, $gsColor3]; + +$gradientFillStops = [ + $gradientStop1, + $gradientStop2, + $gradientStop3, +]; +$gradientFillAngle = 315.0; // 45deg above horiz + +$plotArea->setGradientFillProperties($gradientFillStops, $gradientFillAngle); + +// 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 +); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('A7'); +$chart->setBottomRightPosition('H20'); + +// 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); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/templates/32readwriteScatterChart10.xlsx b/samples/templates/32readwriteScatterChart10.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7d5ac0d280f328c2685e6de590a78a2e30a9cb00 GIT binary patch literal 12363 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 PL;~ysi6~3|^RNF0pR-I- literal 0 HcmV?d00001 diff --git a/samples/templates/32readwriteScatterChart9.xlsx b/samples/templates/32readwriteScatterChart9.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3a0b51b2f85429405f5d07484620113e44ffb455 GIT binary patch literal 12554 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

KR-z20Xg!(?ezsbIY$dW zrc}%URBH?l^bwEt>IawW%z8s~=zU&nRoNims0v&j!m{!5*-$onTpK1T(R7bKdvSW_ zk9mKFQ=u*#L)MePtczXl4@-iW7A#&FTT(x2+8PwzF2dv{3K+{& zz*t84Yb+bu+y5_=fie8&krDUlXC))#4E!1?@Z3$N~l`A$wJe8TBbZ_#wV zX@rqcIuv&Ks-)v+MVc_1T5qb5SmwD>n6j{Q#9-0CQ!hoqCzjvR3n{IIvJ3FAnTqF| z{4VnzT3Fx6V_rVQ$XUnbvg@NM6TiOaI-xey{9&t2CcEH+%%g!E0*z|I+A9PeioHCe zR)(|%z(KJS#a_uWo_({Kv97;N@wSA}8rh}DieHpe*#~t(JUC{VYsTmk9TQ(-!)TQ- zq&!H4$hoGdVU#uwrP(eKFDZ6X3W^p(dZf*Ew^u9h9Rzq>TsFsu;wV`1sBTFF9|tYL zW1ocDGtiL{$j+z+7{hw2e$*&jBY!cBjt!>K3jGM_yFHEZSo=i5-_T|c9&{%1hN093 z5mjb=v_7aL=hUO~^A+V0-ovZWo2)8fU$n`swWFOK(&yOkjppnW00eSJ4>CBggFK5(#n{ETk+%G29UK;;> zPUp8N2nZqEFXR6*weu3^<*deUBtwLM{}cZ)x$zR^We@o`3Nmoh2DZ(rN2!JasM!VSvP%&@UkNL8^IUv7s9{g)?bCzOO%&IsNX2lz_Qd& z-SV;|^%CKw%>9i}Oa2St|0jB10=`s^zX7W${_fa+(UC6!U#hX+fEAQ~`1n_C_R{+A z+UK_|2#7Zo2*`gZp_k@=-){bDE=Btn^FKEqMHxsSi+)ajB7^J$iHN}P^SA#8ZMLTE literal 0 HcmV?d00001 diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 70ff1b3101..bd7082ff68 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -61,6 +61,7 @@ public function __construct() 'horizontal_crosses' => self::HORIZONTAL_CROSSES_AUTOZERO, 'horizontal_crosses_value' => null, 'textRotation' => null, + 'hidden' => null, ]; /** @@ -138,7 +139,8 @@ public function setAxisOptionsProperties( ?string $maximum = null, ?string $majorUnit = null, ?string $minorUnit = null, - ?string $textRotation = null + ?string $textRotation = null, + ?string $hidden = null ): void { $this->axisOptions['axis_labels'] = $axisLabels; $this->setAxisOption('horizontal_crosses_value', $horizontalCrossesValue); @@ -151,6 +153,7 @@ public function setAxisOptionsProperties( $this->setAxisOption('major_unit', $majorUnit); $this->setAxisOption('minor_unit', $minorUnit); $this->setAxisOption('textRotation', $textRotation); + $this->setAxisOption('hidden', $hidden); } /** diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index 3f89e6fbf2..e850f502df 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -144,6 +144,9 @@ class Chart /** @var bool */ private $autoTitleDeleted = false; + /** @var bool */ + private $noFill = false; + /** * Create a new Chart. * majorGridlines and minorGridlines are deprecated, moved to Axis. @@ -747,4 +750,16 @@ public function setAutoTitleDeleted(bool $autoTitleDeleted): self return $this; } + + public function getNoFill(): bool + { + return $this->noFill; + } + + public function setNoFill(bool $noFill): self + { + $this->noFill = $noFill; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/ChartColor.php b/src/PhpSpreadsheet/Chart/ChartColor.php index 7f87e39126..87f3102009 100644 --- a/src/PhpSpreadsheet/Chart/ChartColor.php +++ b/src/PhpSpreadsheet/Chart/ChartColor.php @@ -24,15 +24,18 @@ class ChartColor /** @var ?int */ private $alpha; + /** @var ?int */ + private $brightness; + /** * @param string|string[] $value */ - public function __construct($value = '', ?int $alpha = null, ?string $type = null) + public function __construct($value = '', ?int $alpha = null, ?string $type = null, ?int $brightness = null) { if (is_array($value)) { $this->setColorPropertiesArray($value); } else { - $this->setColorProperties($value, $alpha, $type); + $this->setColorProperties($value, $alpha, $type, $brightness); } } @@ -72,10 +75,23 @@ public function setAlpha(?int $alpha): self return $this; } + public function getBrightness(): ?int + { + return $this->brightness; + } + + public function setBrightness(?int $brightness): self + { + $this->brightness = $brightness; + + return $this; + } + /** * @param null|float|int|string $alpha + * @param null|float|int|string $brightness */ - public function setColorProperties(?string $color, $alpha = null, ?string $type = null): self + public function setColorProperties(?string $color, $alpha = null, ?string $type = null, $brightness = null): self { if (empty($type) && !empty($color)) { if (substr($color, 0, 1) === '*') { @@ -99,6 +115,11 @@ public function setColorProperties(?string $color, $alpha = null, ?string $type } elseif (is_numeric($alpha)) { $this->setAlpha((int) $alpha); } + if ($brightness === null) { + $this->setBrightness(null); + } elseif (is_numeric($brightness)) { + $this->setBrightness((int) $brightness); + } return $this; } @@ -108,7 +129,8 @@ public function setColorPropertiesArray(array $color): self return $this->setColorProperties( $color['value'] ?? '', $color['alpha'] ?? null, - $color['type'] ?? null + $color['type'] ?? null, + $color['brightness'] ?? null ); } @@ -133,6 +155,8 @@ public function getColorProperty($propertyName) $retVal = $this->type; } elseif ($propertyName === 'alpha') { $retVal = $this->alpha; + } elseif ($propertyName === 'brightness') { + $retVal = $this->brightness; } return $retVal; diff --git a/src/PhpSpreadsheet/Chart/DataSeries.php b/src/PhpSpreadsheet/Chart/DataSeries.php index 548145e7eb..5d33e96d09 100644 --- a/src/PhpSpreadsheet/Chart/DataSeries.php +++ b/src/PhpSpreadsheet/Chart/DataSeries.php @@ -94,7 +94,7 @@ class DataSeries private $plotCategory = []; /** - * Smooth Line. + * Smooth Line. Must be specified for both DataSeries and DataSeriesValues. * * @var bool */ diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index bc0e04d193..cb5fa74239 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -536,7 +536,7 @@ public function setBubble3D(bool $bubble3D): self } /** - * Smooth Line. + * Smooth Line. Must be specified for both DataSeries and DataSeriesValues. * * @var bool */ diff --git a/src/PhpSpreadsheet/Chart/PlotArea.php b/src/PhpSpreadsheet/Chart/PlotArea.php index 4bd49ece0f..ccde4bb2aa 100644 --- a/src/PhpSpreadsheet/Chart/PlotArea.php +++ b/src/PhpSpreadsheet/Chart/PlotArea.php @@ -6,6 +6,30 @@ class PlotArea { + /** + * No fill in plot area (show Excel gridlines through chart). + * + * @var bool + */ + private $noFill = false; + + /** + * PlotArea Gradient Stop list. + * Each entry is a 2-element array. + * First is position in %. + * Second is ChartColor. + * + * @var array[] + */ + private $gradientFillStops = []; + + /** + * PlotArea Gradient Angle. + * + * @var ?float + */ + private $gradientFillAngle; + /** * PlotArea Layout. * @@ -101,4 +125,42 @@ public function refresh(Worksheet $worksheet): void $plotSeries->refresh($worksheet); } } + + public function setNoFill(bool $noFill): self + { + $this->noFill = $noFill; + + return $this; + } + + public function getNoFill(): bool + { + return $this->noFill; + } + + public function setGradientFillProperties(array $gradientFillStops, ?float $gradientFillAngle): self + { + $this->gradientFillStops = $gradientFillStops; + $this->gradientFillAngle = $gradientFillAngle; + + return $this; + } + + /** + * Get gradientFillAngle. + */ + public function getGradientFillAngle(): ?float + { + return $this->gradientFillAngle; + } + + /** + * Get gradientFillStops. + * + * @return array + */ + public function getGradientFillStops() + { + return $this->gradientFillStops; + } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index d49a52381e..12ee0ade40 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -73,8 +73,18 @@ public function readChart(SimpleXMLElement $chartElements, $chartName) $xAxis = new Axis(); $yAxis = new Axis(); $autoTitleDeleted = null; + $chartNoFill = false; + $gradientArray = []; + $gradientLin = null; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { + case 'spPr': + $possibleNoFill = $chartElementsC->spPr->children($this->aNamespace); + if (isset($possibleNoFill->noFill)) { + $chartNoFill = true; + } + + break; case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { $chartDetailsC = $chartDetails->children($this->cNamespace); @@ -95,8 +105,29 @@ public function readChart(SimpleXMLElement $chartElements, $chartName) $plotAreaLayout = $XaxisLabel = $YaxisLabel = null; $plotSeries = $plotAttributes = []; $catAxRead = false; + $plotNoFill = false; foreach ($chartDetails as $chartDetailKey => $chartDetail) { switch ($chartDetailKey) { + case 'spPr': + $possibleNoFill = $chartDetails->spPr->children($this->aNamespace); + if (isset($possibleNoFill->noFill)) { + $plotNoFill = true; + } + if (isset($possibleNoFill->gradFill->gsLst)) { + foreach ($possibleNoFill->gradFill->gsLst->gs as $gradient) { + /** @var float */ + $pos = self::getAttribute($gradient, 'pos', 'float'); + $gradientArray[] = [ + $pos / Properties::PERCENTAGE_MULTIPLIER, + new ChartColor($this->readColor($gradient)), + ]; + } + } + if (isset($possibleNoFill->gradFill->lin)) { + $gradientLin = Properties::XmlToAngle((string) self::getAttribute($possibleNoFill->gradFill->lin, 'ang', 'string')); + } + + break; case 'layout': $plotAreaLayout = $this->chartLayoutDetails($chartDetail); @@ -288,6 +319,12 @@ public function readChart(SimpleXMLElement $chartElements, $chartName) } $plotArea = new PlotArea($plotAreaLayout, $plotSeries); $this->setChartAttributes($plotAreaLayout, $plotAttributes); + if ($plotNoFill) { + $plotArea->setNoFill(true); + } + if (!empty($gradientArray)) { + $plotArea->setGradientFillProperties($gradientArray, $gradientLin); + } break; case 'plotVisOnly': @@ -330,6 +367,9 @@ public function readChart(SimpleXMLElement $chartElements, $chartName) } } $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel, $xAxis, $yAxis); + if ($chartNoFill) { + $chart->setNoFill(true); + } if (is_bool($autoTitleDeleted)) { $chart->setAutoTitleDeleted($autoTitleDeleted); } @@ -1147,6 +1187,7 @@ private function readColor(SimpleXMLElement $colorXml): array 'type' => null, 'value' => null, 'alpha' => null, + 'brightness' => null, ]; foreach (ChartColor::EXCEL_COLOR_TYPES as $type) { if (isset($colorXml->$type)) { @@ -1159,6 +1200,13 @@ private function readColor(SimpleXMLElement $colorXml): array $result['alpha'] = ChartColor::alphaFromXml($alpha); } } + if (isset($colorXml->$type->lumMod)) { + /** @var string */ + $brightness = self::getAttribute($colorXml->$type->lumMod, 'val', 'string'); + if (is_numeric($brightness)) { + $result['brightness'] = ChartColor::alphaFromXml($brightness); + } + } break; } @@ -1236,6 +1284,9 @@ private function setAxisProperties(SimpleXMLElement $chartDetail, ?Axis $whichAx if (!isset($whichAxis)) { return; } + if (isset($chartDetail->delete)) { + $whichAxis->setAxisOption('hidden', (string) self::getAttribute($chartDetail->delete, 'val', 'string')); + } if (isset($chartDetail->numFmt)) { $whichAxis->setAxisNumberProperties( (string) self::getAttribute($chartDetail->numFmt, 'formatCode', 'string'), diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index d9d96da691..278b64e7de 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -106,11 +106,17 @@ public function writeChart(\PhpOffice\PhpSpreadsheet\Chart\Chart $chart, $calcul $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); // c:chart + if ($chart->getNoFill()) { + $objWriter->startElement('c:spPr'); + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); // a:noFill + $objWriter->endElement(); // c:spPr + } $this->writePrintSettings($objWriter); - $objWriter->endElement(); + $objWriter->endElement(); // c:chartSpace // Return return $objWriter->getData(); @@ -360,8 +366,35 @@ private function writePlotArea(XMLWriter $objWriter, ?PlotArea $plotArea, ?Title $this->writeSerAxis($objWriter, $id2, $id3); } } + $stops = $plotArea->getGradientFillStops(); + if ($plotArea->getNoFill() || !empty($stops)) { + $objWriter->startElement('c:spPr'); + if ($plotArea->getNoFill()) { + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); // a:noFill + } + if (!empty($stops)) { + $objWriter->startElement('a:gradFill'); + $objWriter->startElement('a:gsLst'); + foreach ($stops as $stop) { + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', (string) (Properties::PERCENTAGE_MULTIPLIER * (float) $stop[0])); + $this->writeColor($objWriter, $stop[1], false); + $objWriter->endElement(); // a:gs + } + $objWriter->endElement(); // a:gsLst + $angle = $plotArea->getGradientFillAngle(); + if ($angle !== null) { + $objWriter->startElement('a:lin'); + $objWriter->writeAttribute('ang', Properties::angleToXml($angle)); + $objWriter->endElement(); // a:lin + } + $objWriter->endElement(); // a:gradFill + } + $objWriter->endElement(); // c:spPr + } - $objWriter->endElement(); + $objWriter->endElement(); // c:plotArea } private function writeDataLabelsBool(XMLWriter $objWriter, string $name, ?bool $value): void @@ -492,7 +525,7 @@ private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, $id $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); - $objWriter->writeAttribute('val', '0'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('hidden') ?? '0'); $objWriter->endElement(); $objWriter->startElement('c:axPos'); @@ -682,7 +715,7 @@ private function writeValueAxis(XMLWriter $objWriter, ?Title $yAxisLabel, $group $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); - $objWriter->writeAttribute('val', '0'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('hidden') ?? '0'); $objWriter->endElement(); $objWriter->startElement('c:axPos'); @@ -1612,7 +1645,18 @@ private function writeColor(XMLWriter $objWriter, ChartColor $chartColor, bool $ if (is_numeric($alpha)) { $objWriter->startElement('a:alpha'); $objWriter->writeAttribute('val', ChartColor::alphaToXml((int) $alpha)); - $objWriter->endElement(); + $objWriter->endElement(); // a:alpha + } + $brightness = $chartColor->getBrightness(); + if (is_numeric($brightness)) { + $brightness = (int) $brightness; + $lumOff = 100 - $brightness; + $objWriter->startElement('a:lumMod'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml($brightness)); + $objWriter->endElement(); // a:lumMod + $objWriter->startElement('a:lumOff'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml($lumOff)); + $objWriter->endElement(); // a:lumOff } $objWriter->endElement(); //a:srgbClr/schemeClr/prstClr if ($solidFill) { diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php index 77b0a9b2e2..fcde7ae359 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\RichText\RichText; @@ -447,4 +448,88 @@ public function testScatter8(): void $reloadedSpreadsheet->disconnectWorksheets(); } + + public function testScatter9(): void + { + // gradient testing + $file = self::DIRECTORY . '32readwriteScatterChart9.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('Worksheet', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertFalse($chart->getNoFill()); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + self::assertFalse($plotArea->getNoFill()); + self::assertEquals(315.0, $plotArea->getGradientFillAngle()); + $stops = $plotArea->getGradientFillStops(); + self::assertCount(3, $stops); + self::assertEquals(0.43808, $stops[0][0]); + self::assertEquals(0, $stops[1][0]); + self::assertEquals(0.91, $stops[2][0]); + $color = $stops[0][1]; + self::assertInstanceOf(ChartColor::class, $color); + self::assertSame('srgbClr', $color->getType()); + self::assertSame('CDDBEC', $color->getValue()); + self::assertNull($color->getAlpha()); + self::assertSame(20, $color->getBrightness()); + $color = $stops[1][1]; + self::assertInstanceOf(ChartColor::class, $color); + self::assertSame('srgbClr', $color->getType()); + self::assertSame('FFC000', $color->getValue()); + self::assertNull($color->getAlpha()); + self::assertNull($color->getBrightness()); + $color = $stops[2][1]; + self::assertInstanceOf(ChartColor::class, $color); + self::assertSame('srgbClr', $color->getType()); + self::assertSame('00B050', $color->getValue()); + self::assertNull($color->getAlpha()); + self::assertSame(4, $color->getBrightness()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testScatter10(): void + { + // nofill for Chart and PlotArea, hidden Axis + $file = self::DIRECTORY . '32readwriteScatterChart10.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('Worksheet', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertTrue($chart->getNoFill()); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + self::assertTrue($plotArea->getNoFill()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } }