Better Handling of Chart DisplayBlanksAs

Fix #4411. User copied some code from a PHPExcel program which set DisplayAsBlanks to `0`. This resulted in what Excel deemed a corrupt spreadsheet using PhpSpreadsheet. The only values allowed for that field are `gap`, `zero` (not `0`), and `span`. PHPExcel used `0` as a default, and got away with it because it ignored the value entirely when writing out the spreadsheet, using `gap` all the time.

I had to choose between throwing an exception and just using the default when an attempt is made to set that property to an invalid value. An exception just seems more punitive than helpful to me, especially if we want people to migrate from PHPExcel, which still seems to have a large user base. So I've gone with using `gap` in place of an invalid value. Note that, according to https://learn.microsoft.com/ru-ru/openspecs/office_standards/ms-oe376/b5c5c694-21d9-437c-9a4a-21e0e843eed8, `gap` is used as the default whenever it is permitted for the chart in question; and, when it isn't permitted, the chart will use its default method (which will always be `zero`).

There were no tests nor samples for this property. All the tests and samples which use it use only `gap`. I have added a small test, and a new sample to illustrate the difference between the 3 options.
This commit is contained in:
oleibman
2025-03-19 20:21:14 -07:00
parent 74dca30c89
commit 19055da8b6
5 changed files with 259 additions and 8 deletions
@@ -0,0 +1,144 @@
<?php
use PhpOffice\PhpSpreadsheet\Chart\Chart;
use PhpOffice\PhpSpreadsheet\Chart\DataSeries;
use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues;
use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend;
use PhpOffice\PhpSpreadsheet\Chart\PlotArea;
use PhpOffice\PhpSpreadsheet\Chart\Title;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
require __DIR__ . '/../Header.php';
$spreadsheet = new Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
$worksheet->fromArray(
[
['', 2010, 2011, 2012],
['Q1', 12, 15, 21],
['Q2', 56, null, 86],
['Q3', 52, 61, 69],
['Q4', 30, 32, 0],
],
strictNullComparison: true
);
// 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
false, // smooth line
DataSeries::STYLE_LINEMARKER // 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);
$title1 = new Title('Test Scatter Chart Gap');
$yAxisLabel1 = new Title('Value ($k)');
// Create the chart
$chart1 = new Chart(
'chart1', // name
$title1, // title
$legend, // legend
$plotArea, // plotArea
true, // plotVisibleOnly
DataSeries::EMPTY_AS_GAP, // displayBlanksAs
null, // xAxisLabel
$yAxisLabel1 // yAxisLabel
);
// Set the position where the chart should appear in the worksheet
$chart1->setTopLeftPosition('A7');
$chart1->setBottomRightPosition('H20');
// Add the chart to the worksheet
$worksheet->addChart($chart1);
$helper->renderChart($chart1, __FILE__);
$title2 = new Title('Test Scatter Chart Zero');
$yAxisLabel2 = new Title('Value ($k)');
// Create the chart
$chart2 = new Chart(
'chart2', // name
$title2, // title
$legend, // legend
$plotArea, // plotArea
true, // plotVisibleOnly
DataSeries::EMPTY_AS_ZERO, // displayBlanksAs
null, // xAxisLabel
$yAxisLabel2 // yAxisLabel
);
// Set the position where the chart should appear in the worksheet
$chart2->setTopLeftPosition('A22');
$chart2->setBottomRightPosition('H35');
// Add the chart to the worksheet
$worksheet->addChart($chart2);
$helper->renderChart($chart2, __FILE__);
$title3 = new Title('Test Scatter Chart Span');
$yAxisLabel3 = new Title('Value ($k)');
// Create the chart
$chart3 = new Chart(
'chart3', // name
$title3, // title
$legend, // legend
$plotArea, // plotArea
true, // plotVisibleOnly
DataSeries::EMPTY_AS_SPAN, // displayBlanksAs
null, // xAxisLabel
$yAxisLabel3 // yAxisLabel
);
// Set the position where the chart should appear in the worksheet
$chart3->setTopLeftPosition('A37');
$chart3->setBottomRightPosition('H50');
// Add the chart to the worksheet
$worksheet->addChart($chart3);
$helper->renderChart($chart3, __FILE__);
// Save Excel 2007 file
$helper->write($spreadsheet, __FILE__, ['Xlsx'], true);
+4 -3
View File
@@ -128,7 +128,7 @@ class Chart
* Create a new Chart.
* majorGridlines and minorGridlines are deprecated, moved to Axis.
*/
public function __construct(string $name, ?Title $title = null, ?Legend $legend = null, ?PlotArea $plotArea = null, bool $plotVisibleOnly = true, string $displayBlanksAs = DataSeries::EMPTY_AS_GAP, ?Title $xAxisLabel = null, ?Title $yAxisLabel = null, ?Axis $xAxis = null, ?Axis $yAxis = null, ?GridLines $majorGridlines = null, ?GridLines $minorGridlines = null)
public function __construct(string $name, ?Title $title = null, ?Legend $legend = null, ?PlotArea $plotArea = null, bool $plotVisibleOnly = true, string $displayBlanksAs = DataSeries::DEFAULT_EMPTY_AS, ?Title $xAxisLabel = null, ?Title $yAxisLabel = null, ?Axis $xAxis = null, ?Axis $yAxis = null, ?GridLines $majorGridlines = null, ?GridLines $minorGridlines = null)
{
$this->name = $name;
$this->title = $title;
@@ -137,7 +137,7 @@ class Chart
$this->yAxisLabel = $yAxisLabel;
$this->plotArea = $plotArea;
$this->plotVisibleOnly = $plotVisibleOnly;
$this->displayBlanksAs = $displayBlanksAs;
$this->setDisplayBlanksAs($displayBlanksAs);
$this->xAxis = $xAxis ?? new Axis();
$this->yAxis = $yAxis ?? new Axis();
if ($majorGridlines !== null) {
@@ -318,7 +318,8 @@ class Chart
*/
public function setDisplayBlanksAs(string $displayBlanksAs): static
{
$this->displayBlanksAs = $displayBlanksAs;
$displayBlanksAs = strtolower($displayBlanksAs);
$this->displayBlanksAs = in_array($displayBlanksAs, DataSeries::VALID_EMPTY_AS, true) ? $displayBlanksAs : DataSeries::DEFAULT_EMPTY_AS;
return $this;
}
+2
View File
@@ -43,6 +43,8 @@ class DataSeries
const EMPTY_AS_GAP = 'gap';
const EMPTY_AS_ZERO = 'zero';
const EMPTY_AS_SPAN = 'span';
const DEFAULT_EMPTY_AS = self::EMPTY_AS_GAP;
const VALID_EMPTY_AS = [self::EMPTY_AS_GAP, self::EMPTY_AS_ZERO, self::EMPTY_AS_SPAN];
/**
* Series Plot Type.
+3 -5
View File
@@ -96,11 +96,9 @@ class Chart extends WriterPart
$objWriter->writeAttribute('val', (string) (int) $chart->getPlotVisibleOnly());
$objWriter->endElement();
if ($chart->getDisplayBlanksAs() !== '') {
$objWriter->startElement('c:dispBlanksAs');
$objWriter->writeAttribute('val', $chart->getDisplayBlanksAs());
$objWriter->endElement();
}
$objWriter->startElement('c:dispBlanksAs');
$objWriter->writeAttribute('val', $chart->getDisplayBlanksAs());
$objWriter->endElement();
$objWriter->startElement('c:showDLblsOverMax');
$objWriter->writeAttribute('val', '0');
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Chart;
use PhpOffice\PhpSpreadsheet\Chart\Chart;
use PhpOffice\PhpSpreadsheet\Chart\DataSeries;
use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues;
use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend;
use PhpOffice\PhpSpreadsheet\Chart\PlotArea;
use PhpOffice\PhpSpreadsheet\Chart\Title;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PHPUnit\Framework\TestCase;
class DisplayBlanksAsTest extends TestCase
{
public function testDisplayBlanksAs(): void
{
$spreadsheet = new Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
$worksheet->fromArray(
[
['', 2010, 2011, 2012],
['Q1', 12, 15, 21],
['Q2', 56, 73, 86],
['Q3', 52, 61, 69],
['Q4', 30, 32, 0],
]
);
$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
];
$xAxisTickValues = [
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4
];
$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_AREACHART, // plotType
DataSeries::GROUPING_PERCENT_STACKED, // plotGrouping
range(0, count($dataSeriesValues) - 1), // plotOrder
$dataSeriesLabels, // plotLabel
$xAxisTickValues, // plotCategory
$dataSeriesValues // plotValues
);
$plotArea = new PlotArea(null, [$series]);
$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false);
$title = new Title('Test %age-Stacked Area Chart');
$yAxisLabel = new Title('Value ($k)');
$chart1 = new Chart(
'chart1', // name
$title, // title
$legend, // legend
$plotArea, // plotArea
true, // plotVisibleOnly
DataSeries::EMPTY_AS_GAP, // displayBlanksAs
null, // xAxisLabel
$yAxisLabel // yAxisLabel
);
self::assertSame(DataSeries::EMPTY_AS_GAP, $chart1->getDisplayBlanksAs());
$chart1->setDisplayBlanksAs(DataSeries::EMPTY_AS_ZERO);
self::assertSame(DataSeries::EMPTY_AS_ZERO, $chart1->getDisplayBlanksAs());
$chart1->setDisplayBlanksAs('0');
self::assertSame(DataSeries::EMPTY_AS_GAP, $chart1->getDisplayBlanksAs(), 'invalid setting converted to default');
$chart2 = new Chart(
'chart2', // name
$title, // title
$legend, // legend
$plotArea, // plotArea
true, // plotVisibleOnly
DataSeries::EMPTY_AS_SPAN, // displayBlanksAs
null, // xAxisLabel
$yAxisLabel // yAxisLabel
);
self::assertSame(DataSeries::EMPTY_AS_SPAN, $chart2->getDisplayBlanksAs());
$chart3 = new Chart(
'chart3', // name
$title, // title
$legend, // legend
$plotArea, // plotArea
true, // plotVisibleOnly
'0', // displayBlanksAs, PHPExcel default
null, // xAxisLabel
$yAxisLabel // yAxisLabel
);
self::assertSame(DataSeries::EMPTY_AS_GAP, $chart3->getDisplayBlanksAs(), 'invalid setting converted to default');
$spreadsheet->disconnectWorksheets();
}
}