From 8fa27d76a91cee73b41c9ca71d13d27e7a910262 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 16 Dec 2025 15:53:46 +0000 Subject: [PATCH 1/3] Added support and options for handling null values in Charts --- src/PhpWord/Style/Chart.php | 96 ++++++++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 22 deletions(-) diff --git a/src/PhpWord/Style/Chart.php b/src/PhpWord/Style/Chart.php index 3773565ce9..ec29ec98ba 100644 --- a/src/PhpWord/Style/Chart.php +++ b/src/PhpWord/Style/Chart.php @@ -73,7 +73,7 @@ class Chart extends AbstractStyle * * @var string */ - private $legendPosition = 'r'; + private $legendPosition = "r"; /** * A list of display options for data labels. @@ -81,13 +81,13 @@ class Chart extends AbstractStyle * @var array */ private $dataLabelOptions = [ - 'showVal' => true, // value - 'showCatName' => true, // category name - 'showLegendKey' => false, //show the cart legend - 'showSerName' => false, // series name - 'showPercent' => false, - 'showLeaderLines' => false, - 'showBubbleSize' => false, + "showVal" => true, // value + "showCatName" => true, // category name + "showLegendKey" => false, //show the cart legend + "showSerName" => false, // series name + "showPercent" => false, + "showLeaderLines" => false, + "showBubbleSize" => false, ]; /** @@ -98,7 +98,7 @@ class Chart extends AbstractStyle * * @var string */ - private $categoryLabelPosition = 'nextTo'; + private $categoryLabelPosition = "nextTo"; /** * A string that tells the writer where to write chart labels or to skip @@ -108,7 +108,7 @@ class Chart extends AbstractStyle * * @var string */ - private $valueLabelPosition = 'nextTo'; + private $valueLabelPosition = "nextTo"; /** * @var string @@ -126,7 +126,7 @@ class Chart extends AbstractStyle * * @var string */ - private $majorTickMarkPos = 'none'; + private $majorTickMarkPos = "none"; /** * Show labels for axis. @@ -149,6 +149,14 @@ class Chart extends AbstractStyle */ private $gridX = false; + /** + * How to display blank values (nulls) in series data. + * Options: 'gap' (break line), 'span' (connect line), 'zero' (plot as zero) + * + * @var string + */ + private $displayBlanksAs = "gap"; // Default to gap + /** * Create a new instance. * @@ -327,10 +335,14 @@ public function getLegendPosition() * * @return self */ - public function setLegendPosition($legendPosition = 'r') + public function setLegendPosition($legendPosition = "r") { - $enum = ['r', 'b', 't', 'l', 'tr']; - $this->legendPosition = $this->setEnumVal($legendPosition, $enum, $this->legendPosition); + $enum = ["r", "b", "t", "l", "tr"]; + $this->legendPosition = $this->setEnumVal( + $legendPosition, + $enum, + $this->legendPosition, + ); return $this; } @@ -354,7 +366,10 @@ public function showAxisLabels() */ public function setShowAxisLabels($value = true) { - $this->showAxisLabels = $this->setBoolVal($value, $this->showAxisLabels); + $this->showAxisLabels = $this->setBoolVal( + $value, + $this->showAxisLabels, + ); return $this; } @@ -381,7 +396,7 @@ public function setDataLabelOptions($values = []): void if (isset($values[$option])) { $this->dataLabelOptions[$option] = $this->setBoolVal( $values[$option], - $this->dataLabelOptions[$option] + $this->dataLabelOptions[$option], ); } } @@ -434,8 +449,12 @@ public function getCategoryLabelPosition() */ public function setCategoryLabelPosition($labelPosition) { - $enum = ['nextTo', 'low', 'high']; - $this->categoryLabelPosition = $this->setEnumVal($labelPosition, $enum, $this->categoryLabelPosition); + $enum = ["nextTo", "low", "high"]; + $this->categoryLabelPosition = $this->setEnumVal( + $labelPosition, + $enum, + $this->categoryLabelPosition, + ); return $this; } @@ -459,8 +478,12 @@ public function getValueLabelPosition() */ public function setValueLabelPosition(string $labelPosition) { - $enum = ['nextTo', 'low', 'high']; - $this->valueLabelPosition = $this->setEnumVal($labelPosition, $enum, $this->valueLabelPosition); + $enum = ["nextTo", "low", "high"]; + $this->valueLabelPosition = $this->setEnumVal( + $labelPosition, + $enum, + $this->valueLabelPosition, + ); return $this; } @@ -521,8 +544,12 @@ public function getMajorTickPosition() */ public function setMajorTickPosition($position): void { - $enum = ['in', 'out', 'cross', 'none']; - $this->majorTickMarkPos = $this->setEnumVal($position, $enum, $this->majorTickMarkPos); + $enum = ["in", "out", "cross", "none"]; + $this->majorTickMarkPos = $this->setEnumVal( + $position, + $enum, + $this->majorTickMarkPos, + ); } /** @@ -548,4 +575,29 @@ public function setShowGridX($value = true) return $this; } + + /** + * Set display blanks as option. + * + * @param string $value 'gap', 'span', or 'zero' + * @return self + */ + public function setDisplayBlanksAs($value) + { + $validValues = ["gap", "span", "zero"]; + if (in_array($value, $validValues)) { + $this->displayBlanksAs = $value; + } + return $this; + } + + /** + * Get display blanks as option. + * + * @return string + */ + public function getDisplayBlanksAs() + { + return $this->displayBlanksAs; + } } From 4d7443bf20e9f548d680e5acdbc35fe465b18c91 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 16 Dec 2025 17:43:35 +0000 Subject: [PATCH 2/3] Tests and documentation updated --- docs/usage/styles/chart.md | 3 +- header-footer-images-test-result.docx | Bin 0 -> 19393 bytes src/PhpWord/Style/Chart.php | 71 ++++++---------- src/PhpWord/Writer/Word2007/Part/Chart.php | 26 ++++-- .../Word2007/Part/ChartNullHandlingTest.php | 77 ++++++++++++++++++ tests/PhpWordTests/_files/tcpdf.pdf | Bin tests/PhpWordTests/_files/temp.epub | Bin 0 -> 136 bytes unknown.file | 0 8 files changed, 126 insertions(+), 51 deletions(-) create mode 100644 header-footer-images-test-result.docx create mode 100644 tests/PhpWordTests/Writer/Word2007/Part/ChartNullHandlingTest.php create mode 100644 tests/PhpWordTests/_files/tcpdf.pdf create mode 100644 tests/PhpWordTests/_files/temp.epub create mode 100644 unknown.file diff --git a/docs/usage/styles/chart.md b/docs/usage/styles/chart.md index f297718d24..99e4016136 100644 --- a/docs/usage/styles/chart.md +++ b/docs/usage/styles/chart.md @@ -16,4 +16,5 @@ Available Chart style options: - ``majorTickMarkPos``. The position for major tick marks, *in*, *out*, *cross*, *none* (default). - ``showAxisLabels``. Show labels for axis, *true* or *false*. - ``gridX``. Show Gridlines for X-Axis, *true* or *false*. -- ``gridY``. Show Gridlines for Y-Axis, *true* or *false*. \ No newline at end of file +- ``gridY``. Show Gridlines for Y-Axis, *true* or *false*. +- ``setDisplayBlanksAs``. How to display null values, *zero*, *span*, *gap* (default). diff --git a/header-footer-images-test-result.docx b/header-footer-images-test-result.docx new file mode 100644 index 0000000000000000000000000000000000000000..a855345141c0a8fe10638bde9213af4a452ed1e0 GIT binary patch literal 19393 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 true, // value - "showCatName" => true, // category name - "showLegendKey" => false, //show the cart legend - "showSerName" => false, // series name - "showPercent" => false, - "showLeaderLines" => false, - "showBubbleSize" => false, + 'showVal' => true, // value + 'showCatName' => true, // category name + 'showLegendKey' => false, //show the cart legend + 'showSerName' => false, // series name + 'showPercent' => false, + 'showLeaderLines' => false, + 'showBubbleSize' => false, ]; /** @@ -98,7 +98,7 @@ class Chart extends AbstractStyle * * @var string */ - private $categoryLabelPosition = "nextTo"; + private $categoryLabelPosition = 'nextTo'; /** * A string that tells the writer where to write chart labels or to skip @@ -108,7 +108,7 @@ class Chart extends AbstractStyle * * @var string */ - private $valueLabelPosition = "nextTo"; + private $valueLabelPosition = 'nextTo'; /** * @var string @@ -126,7 +126,7 @@ class Chart extends AbstractStyle * * @var string */ - private $majorTickMarkPos = "none"; + private $majorTickMarkPos = 'none'; /** * Show labels for axis. @@ -151,11 +151,11 @@ class Chart extends AbstractStyle /** * How to display blank values (nulls) in series data. - * Options: 'gap' (break line), 'span' (connect line), 'zero' (plot as zero) + * Options: 'gap' (break line), 'span' (connect line), 'zero' (plot as zero). * * @var string */ - private $displayBlanksAs = "gap"; // Default to gap + private $displayBlanksAs = 'gap'; // Default to gap /** * Create a new instance. @@ -335,14 +335,10 @@ public function getLegendPosition() * * @return self */ - public function setLegendPosition($legendPosition = "r") + public function setLegendPosition($legendPosition = 'r') { - $enum = ["r", "b", "t", "l", "tr"]; - $this->legendPosition = $this->setEnumVal( - $legendPosition, - $enum, - $this->legendPosition, - ); + $enum = ['r', 'b', 't', 'l', 'tr']; + $this->legendPosition = $this->setEnumVal($legendPosition, $enum, $this->legendPosition); return $this; } @@ -366,10 +362,7 @@ public function showAxisLabels() */ public function setShowAxisLabels($value = true) { - $this->showAxisLabels = $this->setBoolVal( - $value, - $this->showAxisLabels, - ); + $this->showAxisLabels = $this->setBoolVal($value, $this->showAxisLabels); return $this; } @@ -396,7 +389,7 @@ public function setDataLabelOptions($values = []): void if (isset($values[$option])) { $this->dataLabelOptions[$option] = $this->setBoolVal( $values[$option], - $this->dataLabelOptions[$option], + $this->dataLabelOptions[$option] ); } } @@ -449,12 +442,8 @@ public function getCategoryLabelPosition() */ public function setCategoryLabelPosition($labelPosition) { - $enum = ["nextTo", "low", "high"]; - $this->categoryLabelPosition = $this->setEnumVal( - $labelPosition, - $enum, - $this->categoryLabelPosition, - ); + $enum = ['nextTo', 'low', 'high']; + $this->categoryLabelPosition = $this->setEnumVal($labelPosition, $enum, $this->categoryLabelPosition); return $this; } @@ -478,12 +467,8 @@ public function getValueLabelPosition() */ public function setValueLabelPosition(string $labelPosition) { - $enum = ["nextTo", "low", "high"]; - $this->valueLabelPosition = $this->setEnumVal( - $labelPosition, - $enum, - $this->valueLabelPosition, - ); + $enum = ['nextTo', 'low', 'high']; + $this->valueLabelPosition = $this->setEnumVal($labelPosition, $enum, $this->valueLabelPosition); return $this; } @@ -544,12 +529,8 @@ public function getMajorTickPosition() */ public function setMajorTickPosition($position): void { - $enum = ["in", "out", "cross", "none"]; - $this->majorTickMarkPos = $this->setEnumVal( - $position, - $enum, - $this->majorTickMarkPos, - ); + $enum = ['in', 'out', 'cross', 'none']; + $this->majorTickMarkPos = $this->setEnumVal($position, $enum, $this->majorTickMarkPos); } /** @@ -580,14 +561,16 @@ public function setShowGridX($value = true) * Set display blanks as option. * * @param string $value 'gap', 'span', or 'zero' + * * @return self */ public function setDisplayBlanksAs($value) { - $validValues = ["gap", "span", "zero"]; + $validValues = ['gap', 'span', 'zero']; if (in_array($value, $validValues)) { $this->displayBlanksAs = $value; } + return $this; } diff --git a/src/PhpWord/Writer/Word2007/Part/Chart.php b/src/PhpWord/Writer/Word2007/Part/Chart.php index 65e686ebad..445c8272fc 100644 --- a/src/PhpWord/Writer/Word2007/Part/Chart.php +++ b/src/PhpWord/Writer/Word2007/Part/Chart.php @@ -205,6 +205,16 @@ private function writePlotArea(XMLWriter $xmlWriter): void } $xmlWriter->endElement(); // c:plotArea + + $xmlWriter->startElement('c:plotVisOnly'); + $xmlWriter->writeAttribute('val', '1'); + $xmlWriter->endElement(); + + $displayBlanksAs = method_exists($style, 'getDisplayBlanksAs') ? $style->getDisplayBlanksAs() : 'gap'; + + $xmlWriter->startElement('c:dispBlanksAs'); + $xmlWriter->writeAttribute('val', $displayBlanksAs); + $xmlWriter->endElement(); } /** @@ -314,13 +324,17 @@ private function writeSeriesItem(XMLWriter $xmlWriter, $type, $values): void foreach ($values as $value) { $xmlWriter->startElement('c:pt'); $xmlWriter->writeAttribute('idx', $index); - if (\PhpOffice\PhpWord\Settings::isOutputEscapingEnabled()) { - $xmlWriter->writeElement('c:v', $value); - } else { - $xmlWriter->startElement('c:v'); - $xmlWriter->writeRaw($value); - $xmlWriter->endElement(); // c:v + + if ($value !== null) { + if (\PhpOffice\PhpWord\Settings::isOutputEscapingEnabled()) { + $xmlWriter->writeElement('c:v', $value); + } else { + $xmlWriter->startElement('c:v'); + $xmlWriter->writeRaw($value); + $xmlWriter->endElement(); // c:v + } } + $xmlWriter->endElement(); // c:pt ++$index; } diff --git a/tests/PhpWordTests/Writer/Word2007/Part/ChartNullHandlingTest.php b/tests/PhpWordTests/Writer/Word2007/Part/ChartNullHandlingTest.php new file mode 100644 index 0000000000..cf8bbb7b77 --- /dev/null +++ b/tests/PhpWordTests/Writer/Word2007/Part/ChartNullHandlingTest.php @@ -0,0 +1,77 @@ +getStyle(); + + // 1. Default should be 'gap' + self::assertEquals('gap', $style->getDisplayBlanksAs()); + + // 2. Test setting 'span' + $style->setDisplayBlanksAs('span'); + self::assertEquals('span', $style->getDisplayBlanksAs()); + + // 3. Test invalid value (should remain 'span') + $style->setDisplayBlanksAs('invalid_option'); + self::assertEquals('span', $style->getDisplayBlanksAs()); + } + + public function testWriteChartHandlesNullsAndGaps(): void + { + // 1. Setup Data + $categories = ['Jan', 'Feb', 'Mar']; + $values = [10, null, 20]; + $seriesNames = ['My Series']; // <--- ADD THIS + + // 2. Create Chart with Series Name + $chart = new Chart('line', $categories, $values, $seriesNames); + $chart->getStyle()->setDisplayBlanksAs('gap'); + + // 3. Setup Writer + $xmlWriter = new XMLWriter(); + $chartWriter = new ChartWriter(); + + // Mock the parent writer + $chartWriter->setParentWriter($this->createMock(\PhpOffice\PhpWord\Writer\Word2007::class)); + + // 4. Inject Chart into Writer + $reflectionWriter = new ReflectionClass(ChartWriter::class); + $elementProperty = $reflectionWriter->getProperty('element'); + $elementProperty->setAccessible(true); + $elementProperty->setValue($chartWriter, $chart); + + // 5. Run + $method = $reflectionWriter->getMethod('writeChart'); + $method->setAccessible(true); + $method->invokeArgs($chartWriter, [$xmlWriter]); + + $xml = $xmlWriter->getData(); + + // --- ASSERTIONS --- + self::assertStringContainsString('', $xml); + + // Check for the empty point (null value) + self::assertMatchesRegularExpression('//', $xml); + + // Ensure no zero value was written for index 1 + self::assertDoesNotMatchRegularExpression( + '/]*>.*?0<\/c:v>.*?<\/c:pt>/s', + $xml + ); + } +} diff --git a/tests/PhpWordTests/_files/tcpdf.pdf b/tests/PhpWordTests/_files/tcpdf.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/PhpWordTests/_files/temp.epub b/tests/PhpWordTests/_files/temp.epub new file mode 100644 index 0000000000000000000000000000000000000000..f8d3b65aa647437615233b414e66bb7c0c48504b GIT binary patch literal 136 zcmWIWW@h1HU|`^2i0PdWou9Z`M-0dl0gEs& Date: Tue, 16 Dec 2025 17:46:51 +0000 Subject: [PATCH 3/3] Updated changelog --- docs/changes/1.x/1.5.0.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changes/1.x/1.5.0.md b/docs/changes/1.x/1.5.0.md index b96865bada..d2240e7ef1 100644 --- a/docs/changes/1.x/1.5.0.md +++ b/docs/changes/1.x/1.5.0.md @@ -4,6 +4,8 @@ ## Enhancements +- adds the setDisplayBlanksAs option to the Chart Style, allowing users to handle null values in series data. You can choose 'gap' (break line), 'span' (connect line), or 'zero' (display nulls as zero, as it the current default). + ### Bug fixes - Set writeAttribute return type by [@radarhere](https://github.com/radarhere) fixing [#2204](https://github.com/PHPOffice/PHPWord/issues/2204) in [#2776](https://github.com/PHPOffice/PHPWord/pull/2776) @@ -16,4 +18,4 @@ ### BC Breaks -### Notes \ No newline at end of file +### Notes