More Bubble Chart Fixes (#2856)
Continuing the work from #2828, #2841, #2846, and #2852. This is probably my last change in this area for a while. Bubble charts can have bubbles of different sizes. Phpspreadsheet had not supported this. Openpyxl comes with sample code to generate such a chart. I was especially drawn to that solution because its namespace usage would have been unexpected before 2852. And it turned out to come with other surprises - use of absolute paths in the .rels files (PhpSpreadsheet expected only relative), use of a one-cell anchor to place the chart (PhpSpreadsheet expected two-cell anchor or absolute positioning), plaintext in the legend (Phpspreadsheet expected RichText), no cached values for chart data. Excel handles the file okay, and this PR makes sure PhpSpreadsheet does as well. This file is now Samples/Templates/32readwriteBubbleChart2, and is used for both generating a sample output file and in formal tests. A new sample in the 33* series demonstrates how to code these.
This commit is contained in:
parent
273d16e6a3
commit
ab52991dc6
|
|
@ -4547,7 +4547,7 @@ parameters:
|
|||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#"
|
||||
count: 43
|
||||
count: 42
|
||||
path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php
|
||||
|
||||
-
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
<?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\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
require __DIR__ . '/../Header.php';
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
$worksheet->fromArray(
|
||||
[
|
||||
['Number of Products', 'Sales in USD', 'Market share'],
|
||||
[14, 12200, 15],
|
||||
[20, 60000, 33],
|
||||
[18, 24400, 10],
|
||||
[22, 32000, 42],
|
||||
[],
|
||||
[12, 8200, 18],
|
||||
[15, 50000, 30],
|
||||
[19, 22400, 15],
|
||||
[25, 25000, 50],
|
||||
]
|
||||
);
|
||||
|
||||
// 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, null, null, 1, ['2013']), // 2013
|
||||
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['2014']), // 2014
|
||||
];
|
||||
|
||||
// Set the X-Axis values
|
||||
// Datatype
|
||||
// Cell reference for data
|
||||
// Format Code
|
||||
// Number of datapoints in series
|
||||
// Data values
|
||||
// Data Marker
|
||||
$dataSeriesCategories = [
|
||||
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', null, 4),
|
||||
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$7:$A$10', null, 4),
|
||||
];
|
||||
|
||||
// Set the Y-Axis values
|
||||
// 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!$B$7:$B$10', null, 4),
|
||||
];
|
||||
|
||||
// Set the Z-Axis values (bubble size)
|
||||
// Datatype
|
||||
// Cell reference for data
|
||||
// Format Code
|
||||
// Number of datapoints in series
|
||||
// Data values
|
||||
// Data Marker
|
||||
$dataSeriesBubbles = [
|
||||
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4),
|
||||
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$7:$C$10', null, 4),
|
||||
];
|
||||
|
||||
// Build the dataseries
|
||||
$series = new DataSeries(
|
||||
DataSeries::TYPE_BUBBLECHART, // plotType
|
||||
null, // plotGrouping
|
||||
range(0, count($dataSeriesValues) - 1), // plotOrder
|
||||
$dataSeriesLabels, // plotLabel
|
||||
$dataSeriesCategories, // plotCategory
|
||||
$dataSeriesValues // plotValues
|
||||
);
|
||||
$series->setPlotBubbleSizes($dataSeriesBubbles);
|
||||
|
||||
// Set the series in the plot area
|
||||
$plotArea = new PlotArea(null, [$series]);
|
||||
// Set the chart legend
|
||||
$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false);
|
||||
|
||||
// Create the chart
|
||||
$chart = new Chart(
|
||||
'chart1', // name
|
||||
null, // title
|
||||
$legend, // legend
|
||||
$plotArea, // plotArea
|
||||
true, // plotVisibleOnly
|
||||
DataSeries::EMPTY_AS_GAP, // displayBlanksAs
|
||||
null, // xAxisLabel
|
||||
null // yAxisLabel
|
||||
);
|
||||
|
||||
// Set the position where the chart should appear in the worksheet
|
||||
$chart->setTopLeftPosition('E1');
|
||||
$chart->setBottomRightPosition('M15');
|
||||
|
||||
// Add the chart to the worksheet
|
||||
$worksheet->addChart($chart);
|
||||
$worksheet->getColumnDimension('A')->setAutoSize(true);
|
||||
$worksheet->getColumnDimension('B')->setAutoSize(true);
|
||||
$worksheet->getColumnDimension('C')->setAutoSize(true);
|
||||
|
||||
// Save Excel 2007 file
|
||||
$filename = $helper->getFilename(__FILE__);
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||||
$writer->setIncludeCharts(true);
|
||||
$callStartTime = microtime(true);
|
||||
$writer->save($filename);
|
||||
$helper->logWrite($writer, $filename, $callStartTime);
|
||||
Binary file not shown.
|
|
@ -152,6 +152,9 @@ class Chart
|
|||
/** @var ?int */
|
||||
private $perspective;
|
||||
|
||||
/** @var bool */
|
||||
private $oneCellAnchor = false;
|
||||
|
||||
/**
|
||||
* Create a new Chart.
|
||||
*
|
||||
|
|
@ -743,4 +746,16 @@ class Chart
|
|||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOneCellAnchor(): bool
|
||||
{
|
||||
return $this->oneCellAnchor;
|
||||
}
|
||||
|
||||
public function setOneCellAnchor(bool $oneCellAnchor): self
|
||||
{
|
||||
$this->oneCellAnchor = $oneCellAnchor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,13 @@ class DataSeries
|
|||
*/
|
||||
private $plotValues = [];
|
||||
|
||||
/**
|
||||
* Plot Bubble Sizes.
|
||||
*
|
||||
* @var DataSeriesValues[]
|
||||
*/
|
||||
private $plotBubbleSizes = [];
|
||||
|
||||
/**
|
||||
* Create a new DataSeries.
|
||||
*
|
||||
|
|
@ -339,6 +346,28 @@ class DataSeries
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Plot Bubble Sizes.
|
||||
*
|
||||
* @return DataSeriesValues[]
|
||||
*/
|
||||
public function getPlotBubbleSizes(): array
|
||||
{
|
||||
return $this->plotBubbleSizes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Plot Bubble Sizes.
|
||||
*
|
||||
* @param DataSeriesValues[] $plotBubbleSizes
|
||||
*/
|
||||
public function setPlotBubbleSizes(array $plotBubbleSizes): self
|
||||
{
|
||||
$this->plotBubbleSizes = $plotBubbleSizes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Number of Plot Series.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -328,7 +328,7 @@ class DataSeriesValues
|
|||
*/
|
||||
public function isMultiLevelSeries()
|
||||
{
|
||||
if (count($this->dataValues) > 0) {
|
||||
if (!empty($this->dataValues)) {
|
||||
return is_array(array_values($this->dataValues)[0]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -157,6 +157,10 @@ class Xlsx extends BaseReader
|
|||
Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_DRAWING,
|
||||
];
|
||||
|
||||
private const REL_TO_CHART = [
|
||||
Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_CHART,
|
||||
];
|
||||
|
||||
/**
|
||||
* Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
|
||||
*
|
||||
|
|
@ -408,17 +412,21 @@ class Xlsx extends BaseReader
|
|||
|
||||
// Read the theme first, because we need the colour scheme when reading the styles
|
||||
[$workbookBasename, $xmlNamespaceBase] = $this->getWorkbookBaseName();
|
||||
$drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML;
|
||||
$chartNS = self::REL_TO_CHART[$xmlNamespaceBase] ?? Namespaces::CHART;
|
||||
$wbRels = $this->loadZip("xl/_rels/${workbookBasename}.rels", Namespaces::RELATIONSHIPS);
|
||||
$theme = null;
|
||||
$this->styleReader = new Styles();
|
||||
foreach ($wbRels->Relationship as $relx) {
|
||||
$rel = self::getAttributes($relx);
|
||||
$relTarget = (string) $rel['Target'];
|
||||
if (substr($relTarget, 0, 4) === '/xl/') {
|
||||
$relTarget = substr($relTarget, 4);
|
||||
}
|
||||
switch ($rel['Type']) {
|
||||
case "$xmlNamespaceBase/theme":
|
||||
$themeOrderArray = ['lt1', 'dk1', 'lt2', 'dk2'];
|
||||
$themeOrderAdditional = count($themeOrderArray);
|
||||
$drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML;
|
||||
|
||||
$xmlTheme = $this->loadZip("xl/{$relTarget}", $drawingNS);
|
||||
$xmlThemeName = self::getAttributes($xmlTheme);
|
||||
|
|
@ -1204,12 +1212,20 @@ class Xlsx extends BaseReader
|
|||
. '/_rels/'
|
||||
. basename($fileWorksheet)
|
||||
. '.rels';
|
||||
if (substr($drawingFilename, 0, 7) === 'xl//xl/') {
|
||||
$drawingFilename = substr($drawingFilename, 4);
|
||||
}
|
||||
if ($zip->locateName($drawingFilename)) {
|
||||
$relsWorksheet = $this->loadZipNoNamespace($drawingFilename, Namespaces::RELATIONSHIPS);
|
||||
$drawings = [];
|
||||
foreach ($relsWorksheet->Relationship as $ele) {
|
||||
if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") {
|
||||
$drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']);
|
||||
$eleTarget = (string) $ele['Target'];
|
||||
if (substr($eleTarget, 0, 4) === '/xl/') {
|
||||
$drawings[(string) $ele['Id']] = substr($eleTarget, 1);
|
||||
} else {
|
||||
$drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1234,7 +1250,13 @@ class Xlsx extends BaseReader
|
|||
$images[(string) $ele['Id']] = self::dirAdd($fileDrawing, $ele['Target']);
|
||||
} elseif ($eleType === "$xmlNamespaceBase/chart") {
|
||||
if ($this->includeCharts) {
|
||||
$charts[self::dirAdd($fileDrawing, $ele['Target'])] = [
|
||||
$eleTarget = (string) $ele['Target'];
|
||||
if (substr($eleTarget, 0, 4) === '/xl/') {
|
||||
$index = substr($eleTarget, 1);
|
||||
} else {
|
||||
$index = self::dirAdd($fileDrawing, $eleTarget);
|
||||
}
|
||||
$charts[$index] = [
|
||||
'id' => (string) $ele['Id'],
|
||||
'sheet' => $docSheet->getTitle(),
|
||||
];
|
||||
|
|
@ -1326,6 +1348,7 @@ class Xlsx extends BaseReader
|
|||
'width' => $width,
|
||||
'height' => $height,
|
||||
'worksheetTitle' => $docSheet->getTitle(),
|
||||
'oneCellAnchor' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1645,7 +1668,7 @@ class Xlsx extends BaseReader
|
|||
if ($this->includeCharts) {
|
||||
$chartEntryRef = ltrim((string) $contentType['PartName'], '/');
|
||||
$chartElements = $this->loadZip($chartEntryRef);
|
||||
$chartReader = new Chart();
|
||||
$chartReader = new Chart($chartNS, $drawingNS);
|
||||
$objChart = $chartReader->readChart($chartElements, basename($chartEntryRef, '.xml'));
|
||||
if (isset($charts[$chartEntryRef])) {
|
||||
$chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id'];
|
||||
|
|
@ -1662,6 +1685,9 @@ class Xlsx extends BaseReader
|
|||
// oneCellAnchor or absoluteAnchor (e.g. Chart sheet)
|
||||
$objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']);
|
||||
$objChart->setBottomRightPosition('', $chartDetails[$chartPositionRef]['width'], $chartDetails[$chartPositionRef]['height']);
|
||||
if (array_key_exists('oneCellAnchor', $chartDetails[$chartPositionRef])) {
|
||||
$objChart->setOneCellAnchor($chartDetails[$chartPositionRef]['oneCellAnchor']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1823,6 +1849,11 @@ class Xlsx extends BaseReader
|
|||
|
||||
private static function dirAdd($base, $add): string
|
||||
{
|
||||
$add = "$add";
|
||||
if (substr($add, 0, 4) === '/xl/') {
|
||||
$add = substr($add, 4);
|
||||
}
|
||||
|
||||
return (string) preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -305,7 +305,7 @@ class Chart
|
|||
{
|
||||
$multiSeriesType = null;
|
||||
$smoothLine = false;
|
||||
$seriesLabel = $seriesCategory = $seriesValues = $plotOrder = [];
|
||||
$seriesLabel = $seriesCategory = $seriesValues = $plotOrder = $seriesBubbles = [];
|
||||
|
||||
$seriesDetailSet = $chartDetail->children($this->cNamespace);
|
||||
foreach ($seriesDetailSet as $seriesDetailKey => $seriesDetails) {
|
||||
|
|
@ -382,6 +382,10 @@ class Chart
|
|||
case 'yVal':
|
||||
$seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize");
|
||||
|
||||
break;
|
||||
case 'bubbleSize':
|
||||
$seriesBubbles[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize");
|
||||
|
||||
break;
|
||||
case 'bubble3D':
|
||||
$bubble3D = self::getAttribute($seriesDetail, 'val', 'boolean');
|
||||
|
|
@ -435,9 +439,11 @@ class Chart
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
return new DataSeries($plotType, $multiSeriesType, $plotOrder, $seriesLabel, $seriesCategory, $seriesValues, $smoothLine);
|
||||
$series = new DataSeries($plotType, $multiSeriesType, $plotOrder, $seriesLabel, $seriesCategory, $seriesValues, $smoothLine);
|
||||
$series->setPlotBubbleSizes($seriesBubbles);
|
||||
|
||||
return $series;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -494,6 +500,16 @@ class Chart
|
|||
return $seriesValues;
|
||||
}
|
||||
|
||||
if (isset($seriesDetail->v)) {
|
||||
return new DataSeriesValues(
|
||||
DataSeriesValues::DATASERIES_TYPE_STRING,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
[(string) $seriesDetail->v]
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,5 +76,7 @@ class Namespaces
|
|||
|
||||
const PURL_DRAWING = 'http://purl.oclc.org/ooxml/drawingml/main';
|
||||
|
||||
const PURL_CHART = 'http://purl.oclc.org/ooxml/drawingml/chart';
|
||||
|
||||
const PURL_WORKSHEET = 'http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -284,9 +284,12 @@ class Chart extends WriterPart
|
|||
$objWriter->endElement();
|
||||
}
|
||||
} elseif ($chartType === DataSeries::TYPE_BUBBLECHART) {
|
||||
$objWriter->startElement('c:bubbleScale');
|
||||
$objWriter->writeAttribute('val', 25);
|
||||
$objWriter->endElement();
|
||||
$scale = ($plotGroup === null) ? '' : (string) $plotGroup->getPlotStyle();
|
||||
if ($scale !== '') {
|
||||
$objWriter->startElement('c:bubbleScale');
|
||||
$objWriter->writeAttribute('val', $scale);
|
||||
$objWriter->endElement();
|
||||
}
|
||||
|
||||
$objWriter->startElement('c:showNegBubbles');
|
||||
$objWriter->writeAttribute('val', 0);
|
||||
|
|
@ -1326,7 +1329,23 @@ class Chart extends WriterPart
|
|||
}
|
||||
|
||||
if ($groupType === DataSeries::TYPE_BUBBLECHART) {
|
||||
$this->writeBubbles($plotSeriesValues, $objWriter);
|
||||
if (!empty($plotGroup->getPlotBubbleSizes()[$plotSeriesIdx])) {
|
||||
$objWriter->startElement('c:bubbleSize');
|
||||
$this->writePlotSeriesValues(
|
||||
$plotGroup->getPlotBubbleSizes()[$plotSeriesIdx],
|
||||
$objWriter,
|
||||
$groupType,
|
||||
'num'
|
||||
);
|
||||
$objWriter->endElement();
|
||||
if ($plotSeriesValues !== false) {
|
||||
$objWriter->startElement('c:bubble3D');
|
||||
$objWriter->writeAttribute('val', $plotSeriesValues->getBubble3D() ? '1' : '0');
|
||||
$objWriter->endElement();
|
||||
}
|
||||
} else {
|
||||
$this->writeBubbles($plotSeriesValues, $objWriter);
|
||||
}
|
||||
}
|
||||
|
||||
$objWriter->endElement();
|
||||
|
|
@ -1420,38 +1439,43 @@ class Chart extends WriterPart
|
|||
$objWriter->writeRawData($plotSeriesValues->getDataSource());
|
||||
$objWriter->endElement();
|
||||
|
||||
$objWriter->startElement('c:' . $dataType . 'Cache');
|
||||
$count = $plotSeriesValues->getPointCount();
|
||||
$source = $plotSeriesValues->getDataSource();
|
||||
$values = $plotSeriesValues->getDataValues();
|
||||
if ($count > 1 || ($count === 1 && "=$source" !== (string) $values[0])) {
|
||||
$objWriter->startElement('c:' . $dataType . 'Cache');
|
||||
|
||||
if (($groupType != DataSeries::TYPE_PIECHART) && ($groupType != DataSeries::TYPE_PIECHART_3D) && ($groupType != DataSeries::TYPE_DONUTCHART)) {
|
||||
if (($plotSeriesValues->getFormatCode() !== null) && ($plotSeriesValues->getFormatCode() !== '')) {
|
||||
$objWriter->startElement('c:formatCode');
|
||||
$objWriter->writeRawData($plotSeriesValues->getFormatCode());
|
||||
$objWriter->endElement();
|
||||
}
|
||||
}
|
||||
|
||||
$objWriter->startElement('c:ptCount');
|
||||
$objWriter->writeAttribute('val', $plotSeriesValues->getPointCount());
|
||||
$objWriter->endElement();
|
||||
|
||||
$dataValues = $plotSeriesValues->getDataValues();
|
||||
if (!empty($dataValues)) {
|
||||
if (is_array($dataValues)) {
|
||||
foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) {
|
||||
$objWriter->startElement('c:pt');
|
||||
$objWriter->writeAttribute('idx', $plotSeriesKey);
|
||||
|
||||
$objWriter->startElement('c:v');
|
||||
$objWriter->writeRawData($plotSeriesValue);
|
||||
$objWriter->endElement();
|
||||
if (($groupType != DataSeries::TYPE_PIECHART) && ($groupType != DataSeries::TYPE_PIECHART_3D) && ($groupType != DataSeries::TYPE_DONUTCHART)) {
|
||||
if (($plotSeriesValues->getFormatCode() !== null) && ($plotSeriesValues->getFormatCode() !== '')) {
|
||||
$objWriter->startElement('c:formatCode');
|
||||
$objWriter->writeRawData($plotSeriesValues->getFormatCode());
|
||||
$objWriter->endElement();
|
||||
}
|
||||
}
|
||||
|
||||
$objWriter->startElement('c:ptCount');
|
||||
$objWriter->writeAttribute('val', $plotSeriesValues->getPointCount());
|
||||
$objWriter->endElement();
|
||||
|
||||
$dataValues = $plotSeriesValues->getDataValues();
|
||||
if (!empty($dataValues)) {
|
||||
if (is_array($dataValues)) {
|
||||
foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) {
|
||||
$objWriter->startElement('c:pt');
|
||||
$objWriter->writeAttribute('idx', $plotSeriesKey);
|
||||
|
||||
$objWriter->startElement('c:v');
|
||||
$objWriter->writeRawData($plotSeriesValue);
|
||||
$objWriter->endElement();
|
||||
$objWriter->endElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$objWriter->endElement(); // *Cache
|
||||
}
|
||||
|
||||
$objWriter->endElement();
|
||||
|
||||
$objWriter->endElement();
|
||||
$objWriter->endElement(); // *Ref
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -108,6 +108,19 @@ class Drawing extends WriterPart
|
|||
$objWriter->writeElement('xdr:row', (string) ($brColRow[1] - 1));
|
||||
$objWriter->writeElement('xdr:rowOff', self::stringEmu($br['yOffset']));
|
||||
$objWriter->endElement();
|
||||
} elseif ($chart->getOneCellAnchor()) {
|
||||
$objWriter->startElement('xdr:oneCellAnchor');
|
||||
|
||||
$objWriter->startElement('xdr:from');
|
||||
$objWriter->writeElement('xdr:col', (string) ($tlColRow[0] - 1));
|
||||
$objWriter->writeElement('xdr:colOff', self::stringEmu($tl['xOffset']));
|
||||
$objWriter->writeElement('xdr:row', (string) ($tlColRow[1] - 1));
|
||||
$objWriter->writeElement('xdr:rowOff', self::stringEmu($tl['yOffset']));
|
||||
$objWriter->endElement();
|
||||
$objWriter->startElement('xdr:ext');
|
||||
$objWriter->writeAttribute('cx', self::stringEmu($br['xOffset']));
|
||||
$objWriter->writeAttribute('cy', self::stringEmu($br['yOffset']));
|
||||
$objWriter->endElement();
|
||||
} else {
|
||||
$objWriter->startElement('xdr:absoluteAnchor');
|
||||
$objWriter->startElement('xdr:pos');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ChartsOpenpyxlTest extends TestCase
|
||||
{
|
||||
private const DIRECTORY = 'samples' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR;
|
||||
|
||||
public function testBubble2(): void
|
||||
{
|
||||
$file = self::DIRECTORY . '32readwriteBubbleChart2.xlsx';
|
||||
$reader = new XlsxReader();
|
||||
$reader->setIncludeCharts(true);
|
||||
$spreadsheet = $reader->load($file);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
self::assertSame(1, $sheet->getChartCount());
|
||||
|
||||
self::assertSame('Sheet', $sheet->getTitle());
|
||||
$charts = $sheet->getChartCollection();
|
||||
self::assertCount(1, $charts);
|
||||
$chart = $charts[0];
|
||||
self::assertNotNull($chart);
|
||||
self::assertEmpty($chart->getTitle());
|
||||
self::assertTrue($chart->getOneCellAnchor());
|
||||
|
||||
$plotArea = $chart->getPlotArea();
|
||||
$plotSeries = $plotArea->getPlotGroup();
|
||||
self::assertCount(1, $plotSeries);
|
||||
$dataSeries = $plotSeries[0];
|
||||
$labels = $dataSeries->getPlotLabels();
|
||||
self::assertCount(2, $labels);
|
||||
self::assertSame(['2013'], $labels[0]->getDataValues());
|
||||
self::assertSame(['2014'], $labels[1]->getDataValues());
|
||||
|
||||
$plotCategories = $dataSeries->getPlotCategories();
|
||||
self::assertCount(2, $plotCategories);
|
||||
$categories = $plotCategories[0];
|
||||
self::assertSame('Number', $categories->getDataType());
|
||||
self::assertSame('\'Sheet\'!$A$2:$A$5', $categories->getDataSource());
|
||||
self::assertFalse($categories->getBubble3D());
|
||||
$categories = $plotCategories[1];
|
||||
self::assertCount(2, $plotCategories);
|
||||
self::assertSame('Number', $categories->getDataType());
|
||||
self::assertSame('\'Sheet\'!$A$7:$A$10', $categories->getDataSource());
|
||||
self::assertFalse($categories->getBubble3D());
|
||||
|
||||
$plotValues = $dataSeries->getPlotValues();
|
||||
self::assertCount(2, $plotValues);
|
||||
$values = $plotValues[0];
|
||||
self::assertSame('Number', $values->getDataType());
|
||||
self::assertSame('\'Sheet\'!$B$2:$B$5', $values->getDataSource());
|
||||
self::assertFalse($values->getBubble3D());
|
||||
$values = $plotValues[1];
|
||||
self::assertCount(2, $plotValues);
|
||||
self::assertSame('Number', $values->getDataType());
|
||||
self::assertSame('\'Sheet\'!$B$7:$B$10', $values->getDataSource());
|
||||
self::assertFalse($values->getBubble3D());
|
||||
|
||||
$plotValues = $dataSeries->getPlotBubbleSizes();
|
||||
self::assertCount(2, $plotValues);
|
||||
$values = $plotValues[0];
|
||||
self::assertSame('Number', $values->getDataType());
|
||||
self::assertSame('\'Sheet\'!$C$2:$C$5', $values->getDataSource());
|
||||
self::assertFalse($values->getBubble3D());
|
||||
$values = $plotValues[1];
|
||||
self::assertCount(2, $plotValues);
|
||||
self::assertSame('Number', $values->getDataType());
|
||||
self::assertSame('\'Sheet\'!$C$7:$C$10', $values->getDataSource());
|
||||
self::assertFalse($values->getBubble3D());
|
||||
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
public function testXml(): void
|
||||
{
|
||||
$infile = self::DIRECTORY . '32readwriteBubbleChart2.xlsx';
|
||||
$file = 'zip://';
|
||||
$file .= $infile;
|
||||
$file .= '#xl/charts/chart1.xml';
|
||||
$data = file_get_contents($file);
|
||||
// confirm that file contains expected tags
|
||||
if ($data === false) {
|
||||
self::fail('Unable to read file');
|
||||
} else {
|
||||
self::assertSame(0, substr_count($data, 'c:'), 'unusual choice of prefix');
|
||||
self::assertSame(0, substr_count($data, 'bubbleScale'));
|
||||
self::assertSame(1, substr_count($data, '<tx><v>2013</v></tx>'), 'v tag for 2013');
|
||||
self::assertSame(1, substr_count($data, '<tx><v>2014</v></tx>'), 'v tag for 2014');
|
||||
self::assertSame(0, substr_count($data, 'numCache'), 'no cached values');
|
||||
}
|
||||
$file = 'zip://';
|
||||
$file .= $infile;
|
||||
$file .= '#xl/drawings/_rels/drawing1.xml.rels';
|
||||
$data = file_get_contents($file);
|
||||
// confirm that file contains expected tags
|
||||
if ($data === false) {
|
||||
self::fail('Unable to read file');
|
||||
} else {
|
||||
self::assertSame(1, substr_count($data, 'Target="/xl/charts/chart1.xml"'), 'Unusual absolute address in drawing rels file');
|
||||
}
|
||||
$file = 'zip://';
|
||||
$file .= $infile;
|
||||
$file .= '#xl/worksheets/_rels/sheet1.xml.rels';
|
||||
$data = file_get_contents($file);
|
||||
// confirm that file contains expected tags
|
||||
if ($data === false) {
|
||||
self::fail('Unable to read file');
|
||||
} else {
|
||||
self::assertSame(1, substr_count($data, 'Target="/xl/drawings/drawing1.xml"'), 'Unusual absolute address in worksheet rels file');
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue