From 44248cd04ef8bb78235e5fb44c79d773725522cc Mon Sep 17 00:00:00 2001 From: Darren Maczka Date: Sun, 31 Jan 2021 12:53:54 -0500 Subject: [PATCH] Fix/sheets xlsx chart (#1761) * Add support for Google Sheets Exported XLSX Charts Google Sheets XLSX charts use oneCellAnchor positioning and the data series do not have the *Cache elements with cached values. * update CHANGELOG * Add support for Google Sheets Exported XLSX Charts Google Sheets XLSX charts use oneCellAnchor positioning and the data series do not have the *Cache elements with cached values. Because the reader had been assuming *Cache elements existed as children of strRef and numRef, errors about the node being deleted were thrown when reading Xlsx exported from Google Sheets. Co-authored-by: Darren Maczka --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Reader/Xlsx.php | 23 ++++++-- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 45 ++++++++++++---- .../Reader/SheetsXlsxChartTest.php | 51 ++++++++++++++++++ tests/data/Reader/XLSX/sheetsChartsTest.xlsx | Bin 0 -> 12042 bytes 5 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/SheetsXlsxChartTest.php create mode 100755 tests/data/Reader/XLSX/sheetsChartsTest.xlsx diff --git a/CHANGELOG.md b/CHANGELOG.md index efcb21ca3d..d429f0adbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Resolve Google Sheets Xlsx charts issue. Google Sheets uses oneCellAnchor positioning and does not include *Cache values in the exported Xlsx. - Fix for Xls Reader when SST has a bad length [#1592](https://github.com/PHPOffice/PhpSpreadsheet/issues/1592) - Resolve Xlsx loader issue whe hyperlinks don't have a destination - Resolve issues when printer settings resources IDs clash with drawing IDs diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index c05f77de02..c228f344c4 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -1155,13 +1155,27 @@ public function load($pFilename) $this->readHyperLinkDrawing($objDrawing, $oneCellAnchor, $hyperlinks); $objDrawing->setWorksheet($docSheet); - } else { - // ? Can charts be positioned with a oneCellAnchor ? + } elseif ($this->includeCharts && $oneCellAnchor->graphicFrame) { + // Exported XLSX from Google Sheets positions charts with a oneCellAnchor $coordinates = Coordinate::stringFromColumnIndex(((string) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1); $offsetX = Drawing::EMUToPixels($oneCellAnchor->from->colOff); $offsetY = Drawing::EMUToPixels($oneCellAnchor->from->rowOff); $width = Drawing::EMUToPixels(self::getArrayItem($oneCellAnchor->ext->attributes(), 'cx')); $height = Drawing::EMUToPixels(self::getArrayItem($oneCellAnchor->ext->attributes(), 'cy')); + + $graphic = $oneCellAnchor->graphicFrame->children('http://schemas.openxmlformats.org/drawingml/2006/main')->graphic; + /** @var SimpleXMLElement $chartRef */ + $chartRef = $graphic->graphicData->children('http://schemas.openxmlformats.org/drawingml/2006/chart')->chart; + $thisChart = (string) $chartRef->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + + $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [ + 'fromCoordinate' => $coordinates, + 'fromOffsetX' => $offsetX, + 'fromOffsetY' => $offsetY, + 'width' => $width, + 'height' => $height, + 'worksheetTitle' => $docSheet->getTitle(), + ]; } } } @@ -1508,7 +1522,10 @@ public function load($pFilename) $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart); $objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet'])); $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); - $objChart->setBottomRightPosition($chartDetails[$chartPositionRef]['toCoordinate'], $chartDetails[$chartPositionRef]['toOffsetX'], $chartDetails[$chartPositionRef]['toOffsetY']); + if (array_key_exists('toCoordinate', $chartDetails[$chartPositionRef])) { + // For oneCellAnchor positioned charts, toCoordinate is not in the data. Does it need to be calculated? + $objChart->setBottomRightPosition($chartDetails[$chartPositionRef]['toCoordinate'], $chartDetails[$chartPositionRef]['toOffsetX'], $chartDetails[$chartPositionRef]['toOffsetY']); + } } } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index c9a230c215..84b2e62b80 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -328,26 +328,51 @@ private static function chartDataSeriesValueSet($seriesDetail, $namespacesChartM { if (isset($seriesDetail->strRef)) { $seriesSource = (string) $seriesDetail->strRef->f; - $seriesData = self::chartDataSeriesValues($seriesDetail->strRef->strCache->children($namespacesChartMeta['c']), 's'); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker); - return new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, $seriesData['formatCode'], $seriesData['pointCount'], $seriesData['dataValues'], $marker); + if (isset($seriesDetail->strRef->strCache)) { + $seriesData = self::chartDataSeriesValues($seriesDetail->strRef->strCache->children($namespacesChartMeta['c']), 's'); + $seriesValues + ->setFormatCode($seriesData['formatCode']) + ->setDataValues($seriesData['dataValues']); + } + + return $seriesValues; } elseif (isset($seriesDetail->numRef)) { $seriesSource = (string) $seriesDetail->numRef->f; - $seriesData = self::chartDataSeriesValues($seriesDetail->numRef->numCache->children($namespacesChartMeta['c'])); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, null, null, $marker); + if (isset($seriesDetail->strRef->strCache)) { + $seriesData = self::chartDataSeriesValues($seriesDetail->numRef->numCache->children($namespacesChartMeta['c'])); + $seriesValues + ->setFormatCode($seriesData['formatCode']) + ->setDataValues($seriesData['dataValues']); + } - return new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, $seriesData['formatCode'], $seriesData['pointCount'], $seriesData['dataValues'], $marker); + return $seriesValues; } elseif (isset($seriesDetail->multiLvlStrRef)) { $seriesSource = (string) $seriesDetail->multiLvlStrRef->f; - $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($namespacesChartMeta['c']), 's'); - $seriesData['pointCount'] = count($seriesData['dataValues']); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker); - return new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, $seriesData['formatCode'], $seriesData['pointCount'], $seriesData['dataValues'], $marker); + if (isset($seriesDetail->multiLvlStrRef->multiLvlStrCache)) { + $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($namespacesChartMeta['c']), 's'); + $seriesValues + ->setFormatCode($seriesData['formatCode']) + ->setDataValues($seriesData['dataValues']); + } + + return $seriesValues; } elseif (isset($seriesDetail->multiLvlNumRef)) { $seriesSource = (string) $seriesDetail->multiLvlNumRef->f; - $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($namespacesChartMeta['c']), 's'); - $seriesData['pointCount'] = count($seriesData['dataValues']); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker); + + if (isset($seriesDetail->multiLvlNumRef->multiLvlNumCache)) { + $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($namespacesChartMeta['c']), 's'); + $seriesValues + ->setFormatCode($seriesData['formatCode']) + ->setDataValues($seriesData['dataValues']); + } - return new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, $seriesData['formatCode'], $seriesData['pointCount'], $seriesData['dataValues'], $marker); + return $seriesValues; } return null; diff --git a/tests/PhpSpreadsheetTests/Reader/SheetsXlsxChartTest.php b/tests/PhpSpreadsheetTests/Reader/SheetsXlsxChartTest.php new file mode 100644 index 0000000000..c4dc328d2d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/SheetsXlsxChartTest.php @@ -0,0 +1,51 @@ +setIncludeCharts(true); + $spreadsheet = $reader->load($filename); + $worksheet = $spreadsheet->getActiveSheet(); + + $charts = $worksheet->getChartCollection(); + self::assertEquals(2, $worksheet->getChartCount()); + self::assertCount(2, $charts); + + $chart1 = $charts[0]; + $pa1 = $chart1->getPlotArea(); + self::assertEquals(2, $pa1->getPlotSeriesCount()); + + $pg1 = $pa1->getPlotGroup()[0]; + + self::assertEquals(DataSeries::TYPE_LINECHART, $pg1->getPlotType()); + self::assertCount(2, $pg1->getPlotLabels()); + self::assertCount(2, $pg1->getPlotValues()); + self::assertCount(2, $pg1->getPlotCategories()); + + $chart2 = $charts[1]; + $pa1 = $chart2->getPlotArea(); + self::assertEquals(2, $pa1->getPlotSeriesCount()); + + $pg1 = $pa1->getPlotGroupByIndex(0); + //Before a refresh, data values are empty + foreach ($pg1->getPlotValues() as $dv) { + self::assertEmpty($dv->getPointCount()); + } + $pg1->refresh($worksheet); + foreach ($pg1->getPlotValues() as $dv) { + self::assertEquals(9, $dv->getPointCount()); + } + self::assertEquals(DataSeries::TYPE_SCATTERCHART, $pg1->getPlotType()); + self::assertCount(2, $pg1->getPlotLabels()); + self::assertCount(2, $pg1->getPlotValues()); + self::assertCount(2, $pg1->getPlotCategories()); + } +} diff --git a/tests/data/Reader/XLSX/sheetsChartsTest.xlsx b/tests/data/Reader/XLSX/sheetsChartsTest.xlsx new file mode 100755 index 0000000000000000000000000000000000000000..82e00ac932844cc1fa060fd08f84bfb2ea56ec70 GIT binary patch literal 12042 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