From dbea64cb03300abdc236a0b9f4a779ec1a80c2f6 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 11 Jun 2026 06:43:42 -0700 Subject: [PATCH] Chart DataTable Fix #413, which went stale in 2018 and is now reopened. A DataTable can be used on a chart in place of a Legend (or in addition to one, but it doesn't really make sense to have both). The fix was substantially developed by @topaDev before the issue went stale. This PR provides basic support for reading and writing DataTables on charts. The new sample on this PR shows them in action. They have obscure features which are not implemented (e.g. `glow`); those will have to wait for another day. --- samples/Chart33a/33_Chart_create_area_3.php | 108 +++++++++++ src/PhpSpreadsheet/Chart/DataTable.php | 62 +++++++ src/PhpSpreadsheet/Chart/PlotArea.php | 14 ++ src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 35 +++- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 28 +++ .../Chart/DataTableTest.php | 173 ++++++++++++++++++ 6 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 samples/Chart33a/33_Chart_create_area_3.php create mode 100644 src/PhpSpreadsheet/Chart/DataTable.php create mode 100644 tests/PhpSpreadsheetTests/Chart/DataTableTest.php diff --git a/samples/Chart33a/33_Chart_create_area_3.php b/samples/Chart33a/33_Chart_create_area_3.php new file mode 100644 index 000000000..5e5a3f802 --- /dev/null +++ b/samples/Chart33a/33_Chart_create_area_3.php @@ -0,0 +1,108 @@ +getTheme()->setThemeColorName(SpreadsheetTheme::COLOR_SCHEME_2013_2022_NAME); +$worksheet = $spreadsheet->getActiveSheet(); +$sheet2 = $spreadsheet->createSheet(); +$sheet2->setTitle('Sheet2'); +$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 +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$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_AREACHART, // plotType + DataSeries::GROUPING_PERCENT_STACKED, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues // plotValues +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +$plotArea->setDataTable(new DataTable()); +// No need for Legend if using DataTable +$legend = null; //new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test %age-Stacked Area 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('A1'); +$chart->setBottomRightPosition('H18'); + +// Add the chart to the worksheet +$sheet2->addChart($chart); +$spreadsheet->setActiveSheetIndex(1); + +$helper->renderChart($chart, __FILE__); + +// Save Excel 2007 file +$helper->write($spreadsheet, __FILE__, ['Xlsx'], true, null, false); diff --git a/src/PhpSpreadsheet/Chart/DataTable.php b/src/PhpSpreadsheet/Chart/DataTable.php new file mode 100644 index 000000000..71c18fe7c --- /dev/null +++ b/src/PhpSpreadsheet/Chart/DataTable.php @@ -0,0 +1,62 @@ +showHorizontalBorder; + } + + public function getShowVerticalBorder(): bool + { + return $this->showVerticalBorder; + } + + public function getShowOutline(): bool + { + return $this->showOutline; + } + + public function getShowKeys(): bool + { + return $this->showKeys; + } + + public function setShowHorizontalBorder(bool $showHorizontalBorder): self + { + $this->showHorizontalBorder = $showHorizontalBorder; + + return $this; + } + + public function setShowVerticalBorder(bool $showVerticalBorder): self + { + $this->showVerticalBorder = $showVerticalBorder; + + return $this; + } + + public function setShowOutline(bool $showOutline): self + { + $this->showOutline = $showOutline; + + return $this; + } + + public function setShowKeys(bool $showKeys): self + { + $this->showKeys = $showKeys; + + return $this; + } +} diff --git a/src/PhpSpreadsheet/Chart/PlotArea.php b/src/PhpSpreadsheet/Chart/PlotArea.php index e5e390789..05a9ce20e 100644 --- a/src/PhpSpreadsheet/Chart/PlotArea.php +++ b/src/PhpSpreadsheet/Chart/PlotArea.php @@ -38,6 +38,8 @@ class PlotArea */ private array $plotSeries; + private ?DataTable $dataTable = null; + /** * Create a new PlotArea. * @@ -195,6 +197,18 @@ class PlotArea return $this; } + public function setDataTable(DataTable $dataTable): self + { + $this->dataTable = $dataTable; + + return $this; + } + + public function getDataTable(): ?DataTable + { + return $this->dataTable; + } + /** * Implement PHP __clone to create a deep clone, not just a shallow copy. */ diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 4ae3fd4e2..2b6dfb687 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Chart\AxisText; use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; +use PhpOffice\PhpSpreadsheet\Chart\DataTable; use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; @@ -137,7 +138,7 @@ class Chart break; case 'plotArea': - $plotAreaLayout = $XaxisLabel = $YaxisLabel = null; + $plotAreaLayout = $XaxisLabel = $YaxisLabel = $dataTable = null; $plotSeries = $plotAttributes = []; $catAxRead = false; $plotNoFill = false; @@ -358,6 +359,10 @@ class Chart } $plotAttributes = $this->readChartAttributes($chartDetail); + break; + case 'dTable': + $dataTable = $this->readDataTable($chartDetail); + break; } } @@ -365,6 +370,11 @@ class Chart $plotAreaLayout = new Layout(); } $plotArea = new PlotArea($plotAreaLayout, $plotSeries); + if ($dataTable !== null) { + $plotArea->setDataTable( + $dataTable + ); + } $this->setChartAttributes($plotAreaLayout, $plotAttributes); if ($plotNoFill) { $plotArea->setNoFill(true); @@ -1295,6 +1305,29 @@ class Chart return $plotAttributes; } + private function readDataTable(SimpleXMLElement $chartDetail): DataTable + { + $dataTable = new DataTable(); + $temp = self::getAttributeBoolean($chartDetail->showHorzBorder, 'val'); + if ($temp !== null) { + $dataTable->setShowHorizontalBorder($temp); + } + $temp = self::getAttributeBoolean($chartDetail->showVertBorder, 'val'); + if ($temp !== null) { + $dataTable->setShowVerticalBorder($temp); + } + $temp = self::getAttributeBoolean($chartDetail->showOutline, 'val'); + if ($temp !== null) { + $dataTable->setShowOutline($temp); + } + $temp = self::getAttributeBoolean($chartDetail->showKeys, 'val'); + if ($temp !== null) { + $dataTable->setShowKeys($temp); + } + + return $dataTable; + } + /** @param array $plotAttributes */ private function setChartAttributes(Layout $plotArea, array $plotAttributes): void { diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index db8c0321c..1cbc38790 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart as SpreadsheetChart; use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; +use PhpOffice\PhpSpreadsheet\Chart\DataTable; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; @@ -444,6 +445,10 @@ class Chart extends WriterPart $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id2, $id1, $catIsMultiLevelSeries, $xAxis ?? new Axis()); } else { $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis ?? new Axis()); + $dataTable = $plotArea->getDataTable(); + if ($dataTable !== null) { + $this->writeDataTable($objWriter, $dataTable); + } } $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis ?? new Axis()); @@ -482,6 +487,29 @@ class Chart extends WriterPart $objWriter->endElement(); // c:plotArea } + private function writeDataTable(XMLWriter $objWriter, DataTable $dataTable): void + { + $objWriter->startElement('c:dTable'); + + $objWriter->startElement('c:showHorzBorder'); + $objWriter->writeAttribute('val', $dataTable->getShowHorizontalBorder() ? '1' : '0'); + $objWriter->endElement(); + + $objWriter->startElement('c:showVertBorder'); + $objWriter->writeAttribute('val', $dataTable->getShowVerticalBorder() ? '1' : '0'); + $objWriter->endElement(); + + $objWriter->startElement('c:showOutline'); + $objWriter->writeAttribute('val', $dataTable->getShowOutline() ? '1' : '0'); + $objWriter->endElement(); + + $objWriter->startElement('c:showKeys'); + $objWriter->writeAttribute('val', $dataTable->getShowKeys() ? '1' : '0'); + $objWriter->endElement(); + + $objWriter->endElement(); // c:dTable + } + private function writeDataLabelsBool(XMLWriter $objWriter, string $name, ?bool $value): void { if ($value !== null) { diff --git a/tests/PhpSpreadsheetTests/Chart/DataTableTest.php b/tests/PhpSpreadsheetTests/Chart/DataTableTest.php new file mode 100644 index 000000000..f9448a866 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/DataTableTest.php @@ -0,0 +1,173 @@ +getShowHorizontalBorder()); + self::assertTrue($dataTable->getShowVerticalBorder()); + self::assertTrue($dataTable->getShowOutline()); + self::assertTrue($dataTable->getShowKeys()); + $dataTable->setShowHorizontalBorder(false) + ->setShowVerticalBorder(false) + ->setShowOutline(false) + ->setShowKeys(false); + self::assertFalse($dataTable->getShowHorizontalBorder()); + self::assertFalse($dataTable->getShowVerticalBorder()); + self::assertFalse($dataTable->getShowOutline()); + self::assertFalse($dataTable->getShowKeys()); + } + + private function readCharts(XlsxReader $reader): void + { + $reader->setIncludeCharts(true); + } + + private function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testCopyDataTable(): void + { + $spreadsheet = new Spreadsheet(); + // based on 33_Chart_create_area3. + $spreadsheet->getTheme() + ->setThemeColorName( + SpreadsheetTheme::COLOR_SCHEME_2013_2022_NAME + ); + $worksheet = $spreadsheet->getActiveSheet(); + $sheet2 = $spreadsheet->createSheet(); + $sheet2->setTitle('Sheet2'); + $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 + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $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_AREACHART, // plotType + DataSeries::GROUPING_PERCENT_STACKED, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues // plotValues + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + $plotArea->setDataTable(new DataTable()); + // No need for Legend if using DataTable + $legend = null; //new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test %age-Stacked Area 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('A1'); + $chart->setBottomRightPosition('H18'); + + // Add the chart to the worksheet + $sheet2->addChart($chart); + $spreadsheet->setActiveSheetIndex(1); + + // Save Excel 2007 file + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload( + $spreadsheet, + 'Xlsx', + $this->readCharts(...), + $this->writeCharts(...) + ); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $plotArea = $chart2->getPlotArea(); + self::assertNotNull($plotArea); + $dtab = $plotArea->getDataTable(); + self::assertNotNull($dtab); + self::assertTrue($dtab->getShowHorizontalBorder()); + self::assertTrue($dtab->getShowVerticalBorder()); + self::assertTrue($dtab->getShowOutline()); + self::assertTrue($dtab->getShowKeys()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +}