Skip to content

Commit 19055da

Browse files
committed
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.
1 parent 74dca30 commit 19055da

File tree

5 files changed

+259
-8
lines changed

5 files changed

+259
-8
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<?php
2+
3+
use PhpOffice\PhpSpreadsheet\Chart\Chart;
4+
use PhpOffice\PhpSpreadsheet\Chart\DataSeries;
5+
use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues;
6+
use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend;
7+
use PhpOffice\PhpSpreadsheet\Chart\PlotArea;
8+
use PhpOffice\PhpSpreadsheet\Chart\Title;
9+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
10+
11+
require __DIR__ . '/../Header.php';
12+
13+
$spreadsheet = new Spreadsheet();
14+
$worksheet = $spreadsheet->getActiveSheet();
15+
$worksheet->fromArray(
16+
[
17+
['', 2010, 2011, 2012],
18+
['Q1', 12, 15, 21],
19+
['Q2', 56, null, 86],
20+
['Q3', 52, 61, 69],
21+
['Q4', 30, 32, 0],
22+
],
23+
strictNullComparison: true
24+
);
25+
26+
// Set the Labels for each data series we want to plot
27+
// Datatype
28+
// Cell reference for data
29+
// Format Code
30+
// Number of datapoints in series
31+
// Data values
32+
// Data Marker
33+
$dataSeriesLabels = [
34+
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010
35+
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011
36+
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012
37+
];
38+
// Set the X-Axis Labels
39+
$xAxisTickValues = [
40+
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4
41+
];
42+
// Set the Data values for each data series we want to plot
43+
// Datatype
44+
// Cell reference for data
45+
// Format Code
46+
// Number of datapoints in series
47+
// Data values
48+
// Data Marker
49+
$dataSeriesValues = [
50+
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4),
51+
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4),
52+
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4),
53+
];
54+
55+
// Build the dataseries
56+
$series = new DataSeries(
57+
DataSeries::TYPE_SCATTERCHART, // plotType
58+
null, // plotGrouping (Scatter charts don't have any grouping)
59+
range(0, count($dataSeriesValues) - 1), // plotOrder
60+
$dataSeriesLabels, // plotLabel
61+
$xAxisTickValues, // plotCategory
62+
$dataSeriesValues, // plotValues
63+
null, // plotDirection
64+
false, // smooth line
65+
DataSeries::STYLE_LINEMARKER // plotStyle
66+
);
67+
68+
// Set the series in the plot area
69+
$plotArea = new PlotArea(null, [$series]);
70+
// Set the chart legend
71+
$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false);
72+
73+
$title1 = new Title('Test Scatter Chart Gap');
74+
$yAxisLabel1 = new Title('Value ($k)');
75+
// Create the chart
76+
$chart1 = new Chart(
77+
'chart1', // name
78+
$title1, // title
79+
$legend, // legend
80+
$plotArea, // plotArea
81+
true, // plotVisibleOnly
82+
DataSeries::EMPTY_AS_GAP, // displayBlanksAs
83+
null, // xAxisLabel
84+
$yAxisLabel1 // yAxisLabel
85+
);
86+
87+
// Set the position where the chart should appear in the worksheet
88+
$chart1->setTopLeftPosition('A7');
89+
$chart1->setBottomRightPosition('H20');
90+
91+
// Add the chart to the worksheet
92+
$worksheet->addChart($chart1);
93+
94+
$helper->renderChart($chart1, __FILE__);
95+
96+
$title2 = new Title('Test Scatter Chart Zero');
97+
$yAxisLabel2 = new Title('Value ($k)');
98+
// Create the chart
99+
$chart2 = new Chart(
100+
'chart2', // name
101+
$title2, // title
102+
$legend, // legend
103+
$plotArea, // plotArea
104+
true, // plotVisibleOnly
105+
DataSeries::EMPTY_AS_ZERO, // displayBlanksAs
106+
null, // xAxisLabel
107+
$yAxisLabel2 // yAxisLabel
108+
);
109+
110+
// Set the position where the chart should appear in the worksheet
111+
$chart2->setTopLeftPosition('A22');
112+
$chart2->setBottomRightPosition('H35');
113+
114+
// Add the chart to the worksheet
115+
$worksheet->addChart($chart2);
116+
117+
$helper->renderChart($chart2, __FILE__);
118+
119+
$title3 = new Title('Test Scatter Chart Span');
120+
$yAxisLabel3 = new Title('Value ($k)');
121+
122+
// Create the chart
123+
$chart3 = new Chart(
124+
'chart3', // name
125+
$title3, // title
126+
$legend, // legend
127+
$plotArea, // plotArea
128+
true, // plotVisibleOnly
129+
DataSeries::EMPTY_AS_SPAN, // displayBlanksAs
130+
null, // xAxisLabel
131+
$yAxisLabel3 // yAxisLabel
132+
);
133+
134+
// Set the position where the chart should appear in the worksheet
135+
$chart3->setTopLeftPosition('A37');
136+
$chart3->setBottomRightPosition('H50');
137+
138+
// Add the chart to the worksheet
139+
$worksheet->addChart($chart3);
140+
141+
$helper->renderChart($chart3, __FILE__);
142+
143+
// Save Excel 2007 file
144+
$helper->write($spreadsheet, __FILE__, ['Xlsx'], true);

src/PhpSpreadsheet/Chart/Chart.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ class Chart
128128
* Create a new Chart.
129129
* majorGridlines and minorGridlines are deprecated, moved to Axis.
130130
*/
131-
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)
131+
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)
132132
{
133133
$this->name = $name;
134134
$this->title = $title;
@@ -137,7 +137,7 @@ public function __construct(string $name, ?Title $title = null, ?Legend $legend
137137
$this->yAxisLabel = $yAxisLabel;
138138
$this->plotArea = $plotArea;
139139
$this->plotVisibleOnly = $plotVisibleOnly;
140-
$this->displayBlanksAs = $displayBlanksAs;
140+
$this->setDisplayBlanksAs($displayBlanksAs);
141141
$this->xAxis = $xAxis ?? new Axis();
142142
$this->yAxis = $yAxis ?? new Axis();
143143
if ($majorGridlines !== null) {
@@ -318,7 +318,8 @@ public function getDisplayBlanksAs(): string
318318
*/
319319
public function setDisplayBlanksAs(string $displayBlanksAs): static
320320
{
321-
$this->displayBlanksAs = $displayBlanksAs;
321+
$displayBlanksAs = strtolower($displayBlanksAs);
322+
$this->displayBlanksAs = in_array($displayBlanksAs, DataSeries::VALID_EMPTY_AS, true) ? $displayBlanksAs : DataSeries::DEFAULT_EMPTY_AS;
322323

323324
return $this;
324325
}

src/PhpSpreadsheet/Chart/DataSeries.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ class DataSeries
4343
const EMPTY_AS_GAP = 'gap';
4444
const EMPTY_AS_ZERO = 'zero';
4545
const EMPTY_AS_SPAN = 'span';
46+
const DEFAULT_EMPTY_AS = self::EMPTY_AS_GAP;
47+
const VALID_EMPTY_AS = [self::EMPTY_AS_GAP, self::EMPTY_AS_ZERO, self::EMPTY_AS_SPAN];
4648

4749
/**
4850
* Series Plot Type.

src/PhpSpreadsheet/Writer/Xlsx/Chart.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,9 @@ public function writeChart(\PhpOffice\PhpSpreadsheet\Chart\Chart $chart, bool $c
9696
$objWriter->writeAttribute('val', (string) (int) $chart->getPlotVisibleOnly());
9797
$objWriter->endElement();
9898

99-
if ($chart->getDisplayBlanksAs() !== '') {
100-
$objWriter->startElement('c:dispBlanksAs');
101-
$objWriter->writeAttribute('val', $chart->getDisplayBlanksAs());
102-
$objWriter->endElement();
103-
}
99+
$objWriter->startElement('c:dispBlanksAs');
100+
$objWriter->writeAttribute('val', $chart->getDisplayBlanksAs());
101+
$objWriter->endElement();
104102

105103
$objWriter->startElement('c:showDLblsOverMax');
106104
$objWriter->writeAttribute('val', '0');
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Chart;
6+
7+
use PhpOffice\PhpSpreadsheet\Chart\Chart;
8+
use PhpOffice\PhpSpreadsheet\Chart\DataSeries;
9+
use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues;
10+
use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend;
11+
use PhpOffice\PhpSpreadsheet\Chart\PlotArea;
12+
use PhpOffice\PhpSpreadsheet\Chart\Title;
13+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
14+
use PHPUnit\Framework\TestCase;
15+
16+
class DisplayBlanksAsTest extends TestCase
17+
{
18+
public function testDisplayBlanksAs(): void
19+
{
20+
$spreadsheet = new Spreadsheet();
21+
$worksheet = $spreadsheet->getActiveSheet();
22+
$worksheet->fromArray(
23+
[
24+
['', 2010, 2011, 2012],
25+
['Q1', 12, 15, 21],
26+
['Q2', 56, 73, 86],
27+
['Q3', 52, 61, 69],
28+
['Q4', 30, 32, 0],
29+
]
30+
);
31+
32+
$dataSeriesLabels = [
33+
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010
34+
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011
35+
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012
36+
];
37+
38+
$xAxisTickValues = [
39+
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4
40+
];
41+
42+
$dataSeriesValues = [
43+
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4),
44+
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4),
45+
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4),
46+
];
47+
48+
// Build the dataseries
49+
$series = new DataSeries(
50+
DataSeries::TYPE_AREACHART, // plotType
51+
DataSeries::GROUPING_PERCENT_STACKED, // plotGrouping
52+
range(0, count($dataSeriesValues) - 1), // plotOrder
53+
$dataSeriesLabels, // plotLabel
54+
$xAxisTickValues, // plotCategory
55+
$dataSeriesValues // plotValues
56+
);
57+
58+
$plotArea = new PlotArea(null, [$series]);
59+
$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false);
60+
61+
$title = new Title('Test %age-Stacked Area Chart');
62+
$yAxisLabel = new Title('Value ($k)');
63+
64+
$chart1 = new Chart(
65+
'chart1', // name
66+
$title, // title
67+
$legend, // legend
68+
$plotArea, // plotArea
69+
true, // plotVisibleOnly
70+
DataSeries::EMPTY_AS_GAP, // displayBlanksAs
71+
null, // xAxisLabel
72+
$yAxisLabel // yAxisLabel
73+
);
74+
self::assertSame(DataSeries::EMPTY_AS_GAP, $chart1->getDisplayBlanksAs());
75+
$chart1->setDisplayBlanksAs(DataSeries::EMPTY_AS_ZERO);
76+
self::assertSame(DataSeries::EMPTY_AS_ZERO, $chart1->getDisplayBlanksAs());
77+
$chart1->setDisplayBlanksAs('0');
78+
self::assertSame(DataSeries::EMPTY_AS_GAP, $chart1->getDisplayBlanksAs(), 'invalid setting converted to default');
79+
80+
$chart2 = new Chart(
81+
'chart2', // name
82+
$title, // title
83+
$legend, // legend
84+
$plotArea, // plotArea
85+
true, // plotVisibleOnly
86+
DataSeries::EMPTY_AS_SPAN, // displayBlanksAs
87+
null, // xAxisLabel
88+
$yAxisLabel // yAxisLabel
89+
);
90+
self::assertSame(DataSeries::EMPTY_AS_SPAN, $chart2->getDisplayBlanksAs());
91+
92+
$chart3 = new Chart(
93+
'chart3', // name
94+
$title, // title
95+
$legend, // legend
96+
$plotArea, // plotArea
97+
true, // plotVisibleOnly
98+
'0', // displayBlanksAs, PHPExcel default
99+
null, // xAxisLabel
100+
$yAxisLabel // yAxisLabel
101+
);
102+
self::assertSame(DataSeries::EMPTY_AS_GAP, $chart3->getDisplayBlanksAs(), 'invalid setting converted to default');
103+
104+
$spreadsheet->disconnectWorksheets();
105+
}
106+
}

0 commit comments

Comments
 (0)