Chart Dynamic Title and Special Font Properties (#3800)

* Chart Dynamic Title and Special Font Properties

Fix #3797. Excel allows a Chart Title to be a formula, albeit a very rigidly limited one. It can only be a reference to a single cell, and the worksheet name must be specified, and the column and row must be absolute. Methods are added to Chart/Title to accommodate this (and styling for it). This will be handled for input/output for Xlsx, and for output for Html.

The sample file which was submitted with this issue demonstrated that something else was missing. When setting the font for a chart title in Excel, you can specify all-caps or small-caps, options not available for most cell formatting. These are now added.

The sample file also fell into the category of spreadsheets which lose one or more charts when converted to Html. I have redone the "extend rows and charts" logic in Html Writer. It is now clearer (I hope) and more efficient, and hopefully this problem will not arise again.

* Scrutinizer 50/50

One false positive, one correct "unused parameter".
This commit is contained in:
oleibman
2023-11-30 08:01:56 -08:00
committed by GitHub
parent 7fbf5c494f
commit 009e009811
11 changed files with 481 additions and 100 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ foreach ($inputFileNames as $inputFileName) {
foreach ($chartNames as $i => $chartName) {
$chart = $worksheet->getChartByName($chartName);
if ($chart->getTitle() !== null) {
$caption = '"' . implode(' ', $chart->getTitle()->getCaption()) . '"';
$caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"';
} else {
$caption = 'Untitled';
}
+83
View File
@@ -0,0 +1,83 @@
<?php
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Settings;
require __DIR__ . '/../Header.php';
$inputFileType = 'Xlsx';
$inputFileName = __DIR__ . '/../templates/37dynamictitle.xlsx';
var_dump($inputFileName);
var_dump(realpath($inputFileName));
$inputFileNames = [$inputFileName];
foreach ($inputFileNames as $inputFileName) {
$inputFileNameShort = basename($inputFileName);
if (!file_exists($inputFileName)) {
$helper->log('File ' . $inputFileNameShort . ' does not exist');
continue;
}
$reader = IOFactory::createReader($inputFileType);
$reader->setIncludeCharts(true);
$callStartTime = microtime(true);
$spreadsheet = $reader->load($inputFileName);
$helper->logRead($inputFileType, $inputFileName, $callStartTime);
$helper->log('Iterate worksheets looking at the charts');
foreach ($spreadsheet->getWorksheetIterator() as $worksheet) {
$sheetName = $worksheet->getTitle();
$worksheet->getCell('A1')->setValue('Changed Title');
$helper->log('Worksheet: ' . $sheetName);
$chartNames = $worksheet->getChartNames();
if (empty($chartNames)) {
$helper->log(' There are no charts in this worksheet');
} else {
natsort($chartNames);
foreach ($chartNames as $i => $chartName) {
$chart = $worksheet->getChartByName($chartName);
if ($chart->getTitle() !== null) {
$caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"';
} else {
$caption = 'Untitled';
}
$helper->log(' ' . $chartName . ' - ' . $caption);
$indentation = str_repeat(' ', strlen($chartName) + 3);
$groupCount = $chart->getPlotArea()->getPlotGroupCount();
if ($groupCount == 1) {
$chartType = $chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType();
$helper->log($indentation . ' ' . $chartType);
$helper->renderChart($chart, __FILE__, $spreadsheet);
} else {
$chartTypes = [];
for ($i = 0; $i < $groupCount; ++$i) {
$chartTypes[] = $chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType();
}
$chartTypes = array_unique($chartTypes);
if (count($chartTypes) == 1) {
$chartType = 'Multiple Plot ' . array_pop($chartTypes);
$helper->log($indentation . ' ' . $chartType);
$helper->renderChart($chart, __FILE__);
} elseif (count($chartTypes) == 0) {
$helper->log($indentation . ' *** Type not yet implemented');
} else {
$helper->log($indentation . ' Combination Chart');
$helper->renderChart($chart, __FILE__);
}
}
}
}
}
$callStartTime = microtime(true);
$helper->write($spreadsheet, $inputFileName, ['Xlsx'], true);
Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class);
$callStartTime = microtime(true);
$helper->write($spreadsheet, $inputFileName, ['Html'], true);
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
Binary file not shown.
+56 -2
View File
@@ -3,9 +3,16 @@
namespace PhpOffice\PhpSpreadsheet\Chart;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Font;
class Title
{
public const TITLE_CELL_REFERENCE
= '/^(.*)!' // beginning of string, everything up to ! is match[1]
. '[$]([A-Z]{1,3})' // absolute column string match[2]
. '[$](\d{1,7})$/i'; // absolute row string match[3]
/**
* Title Caption.
*
@@ -25,6 +32,10 @@ class Title
*/
private ?Layout $layout;
private string $cellReference = '';
private ?Font $font = null;
/**
* Create a new Title.
*
@@ -48,8 +59,14 @@ class Title
return $this->caption;
}
public function getCaptionText(): string
public function getCaptionText(?Spreadsheet $spreadsheet = null): string
{
if ($spreadsheet !== null) {
$caption = $this->getCalculatedTitle($spreadsheet);
if ($caption !== null) {
return $caption;
}
}
$caption = $this->caption;
if (is_string($caption)) {
return $caption;
@@ -100,13 +117,50 @@ class Title
*
* @param bool $overlay
*/
public function setOverlay($overlay): void
public function setOverlay($overlay): static
{
$this->overlay = $overlay;
return $this;
}
public function getLayout(): ?Layout
{
return $this->layout;
}
public function setCellReference(string $cellReference): self
{
$this->cellReference = $cellReference;
return $this;
}
public function getCellReference(): string
{
return $this->cellReference;
}
public function getCalculatedTitle(?Spreadsheet $spreadsheet): ?string
{
preg_match(self::TITLE_CELL_REFERENCE, $this->cellReference, $matches);
if (count($matches) === 0 || $spreadsheet === null) {
return null;
}
$sheetName = preg_replace("/^'(.*)'$/", '$1', $matches[1]) ?? '';
return $spreadsheet->getSheetByName($sheetName)?->getCell($matches[2] . $matches[3])?->getFormattedValue();
}
public function getFont(): ?Font
{
return $this->font;
}
public function setFont(?Font $font): self
{
$this->font = $font;
return $this;
}
}
+17 -2
View File
@@ -198,7 +198,7 @@ class Sample
*
* @codeCoverageIgnore
*/
public function renderChart(Chart $chart, string $fileName): void
public function renderChart(Chart $chart, string $fileName, ?Spreadsheet $spreadsheet = null): void
{
if ($this->isCli() === true) {
return;
@@ -206,17 +206,32 @@ class Sample
Settings::setChartRenderer(MtJpGraphRenderer::class);
$fileName = $this->getFilename($fileName, 'png');
$title = $chart->getTitle();
$caption = null;
if ($title !== null) {
$calculatedTitle = $title->getCalculatedTitle($spreadsheet);
if ($calculatedTitle !== null) {
$caption = $title->getCaption();
$title->setCaption($calculatedTitle);
}
}
try {
$chart->render($fileName);
$this->log('Rendered image: ' . $fileName);
$imageData = file_get_contents($fileName);
$imageData = @file_get_contents($fileName);
if ($imageData !== false) {
echo '<div><img src="data:image/gif;base64,' . base64_encode($imageData) . '" /></div>';
} else {
$this->log('Unable to open chart' . PHP_EOL);
}
} catch (Throwable $e) {
$this->log('Error rendering chart: ' . $e->getMessage() . PHP_EOL);
}
if (isset($title, $caption)) {
$title->setCaption($caption);
}
Settings::unsetChartRenderer();
}
public function titles(string $category, string $functionName, ?string $description = null): void
+20 -1
View File
@@ -497,6 +497,8 @@ class Chart
$caption = [];
$titleLayout = null;
$titleOverlay = false;
$titleFormula = null;
$titleFont = null;
foreach ($titleDetails as $titleDetailKey => $chartDetail) {
$chartDetail = Xlsx::testSimpleXml($chartDetail);
switch ($titleDetailKey) {
@@ -517,6 +519,9 @@ class Chart
$caption[] = (string) $pt->v;
}
}
if (isset($chartDetail->strRef->f)) {
$titleFormula = (string) $chartDetail->strRef->f;
}
}
break;
@@ -527,11 +532,24 @@ class Chart
case 'layout':
$titleLayout = $this->chartLayoutDetails($chartDetail);
break;
case 'txPr':
if (isset($chartDetail->children($this->aNamespace)->p)) {
$titleFont = $this->parseFont($chartDetail->children($this->aNamespace)->p);
}
break;
}
}
$title = new Title($caption, $titleLayout, (bool) $titleOverlay);
if (!empty($titleFormula)) {
$title->setCellReference($titleFormula);
}
if ($titleFont !== null) {
$title->setFont($titleFont);
}
return new Title($caption, $titleLayout, (bool) $titleOverlay);
return $title;
}
private function chartLayoutDetails(SimpleXMLElement $chartDetail): ?Layout
@@ -1185,6 +1203,7 @@ class Chart
$fontArray['italic'] = self::getAttributeBoolean($titleDetailPart->pPr->defRPr, 'i');
$fontArray['underscore'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'u');
$fontArray['strikethrough'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'strike');
$fontArray['cap'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'cap');
if (isset($titleDetailPart->pPr->defRPr->latin)) {
$fontArray['latin'] = self::getAttributeString($titleDetailPart->pPr->defRPr->latin, 'typeface');
+5
View File
@@ -78,6 +78,11 @@ class Settings
self::$chartRenderer = $rendererClassName;
}
public static function unsetChartRenderer(): void
{
self::$chartRenderer = null;
}
/**
* Return the Chart Rendering Library that PhpSpreadsheet is currently configured to use.
*
+31
View File
@@ -13,6 +13,13 @@ class Font extends Supervisor
const UNDERLINE_SINGLE = 'single';
const UNDERLINE_SINGLEACCOUNTING = 'singleAccounting';
const CAP_ALL = 'all';
const CAP_SMALL = 'small';
const CAP_NONE = 'none';
private const VALID_CAPS = [self::CAP_ALL, self::CAP_SMALL, self::CAP_NONE];
protected ?string $cap = null;
/**
* Font Name.
*
@@ -236,6 +243,9 @@ class Font extends Supervisor
if (isset($styleArray['scheme'])) {
$this->setScheme($styleArray['scheme']);
}
if (isset($styleArray['cap'])) {
$this->setCap($styleArray['cap']);
}
}
return $this;
@@ -795,6 +805,7 @@ class Font extends Supervisor
$this->hashChartColor($this->chartColor),
$this->hashChartColor($this->underlineColor),
(string) $this->baseLine,
(string) $this->cap,
]
)
. __CLASS__
@@ -806,6 +817,7 @@ class Font extends Supervisor
$exportedArray = [];
$this->exportArray2($exportedArray, 'baseLine', $this->getBaseLine());
$this->exportArray2($exportedArray, 'bold', $this->getBold());
$this->exportArray2($exportedArray, 'cap', $this->getCap());
$this->exportArray2($exportedArray, 'chartColor', $this->getChartColor());
$this->exportArray2($exportedArray, 'color', $this->getColor());
$this->exportArray2($exportedArray, 'complexScript', $this->getComplexScript());
@@ -847,4 +859,23 @@ class Font extends Supervisor
return $this;
}
/**
* Set capitalization attribute. If not one of the permitted
* values (all, small, or none), set it to null.
* This will be honored only for the font for chart titles.
* None is distinguished from null because null will inherit
* the current value, whereas 'none' will override it.
*/
public function setCap(string $cap): self
{
$this->cap = in_array($cap, self::VALID_CAPS, true) ? $cap : null;
return $this;
}
public function getCap(): ?string
{
return $this->cap;
}
}
+61 -91
View File
@@ -23,6 +23,7 @@ use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Font;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Style\Style;
use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
@@ -150,6 +151,12 @@ class Html extends BaseWriter
*/
private $editHtmlCallback;
/** @var BaseDrawing[] */
private $sheetDrawings;
/** @var Chart[] */
private $sheetCharts;
/**
* Create a new HTML.
*/
@@ -479,11 +486,14 @@ class Html extends BaseWriter
foreach ($sheets as $sheet) {
// Write table header
$html .= $this->generateTableHeader($sheet);
$this->sheetCharts = [];
$this->sheetDrawings = [];
// Get worksheet dimension
[$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension());
[$minCol, $minRow] = Coordinate::indexesFromString($min);
[$maxCol, $maxRow] = Coordinate::indexesFromString($max);
$this->extendRowsAndColumns($sheet, $maxCol, $maxRow);
[$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow);
@@ -510,8 +520,6 @@ class Html extends BaseWriter
$html .= $endTag;
}
--$row;
$html .= $this->extendRowsForChartsAndImages($sheet, $row);
// Write table footer
$html .= $this->generateTableFooter();
@@ -563,78 +571,33 @@ class Html extends BaseWriter
return $html;
}
/**
* Extend Row if chart is placed after nominal end of row.
* This code should be exercised by sample:
* Chart/32_Chart_read_write_PDF.php.
*
* @param int $row Row to check for charts
*/
private function extendRowsForCharts(Worksheet $worksheet, int $row): array
private function extendRowsAndColumns(Worksheet $worksheet, int &$colMax, int &$rowMax): void
{
$rowMax = $row;
$colMax = 'A';
$anyfound = false;
if ($this->includeCharts) {
foreach ($worksheet->getChartCollection() as $chart) {
if ($chart instanceof Chart) {
$anyfound = true;
$chartCoordinates = $chart->getTopLeftPosition();
$chartTL = Coordinate::coordinateFromString($chartCoordinates['cell']);
$chartCol = Coordinate::columnIndexFromString($chartTL[0]);
$this->sheetCharts[$chartCoordinates['cell']] = $chart;
$chartTL = Coordinate::indexesFromString($chartCoordinates['cell']);
if ($chartTL[1] > $rowMax) {
$rowMax = $chartTL[1];
}
if ($chartCol > Coordinate::columnIndexFromString($colMax)) {
if ($chartTL[0] > $colMax) {
$colMax = $chartTL[0];
}
}
}
}
return [$rowMax, $colMax, $anyfound];
}
private function extendRowsForChartsAndImages(Worksheet $worksheet, int $row): string
{
[$rowMax, $colMax, $anyfound] = $this->extendRowsForCharts($worksheet, $row);
foreach ($worksheet->getDrawingCollection() as $drawing) {
$anyfound = true;
$imageTL = Coordinate::coordinateFromString($drawing->getCoordinates());
$imageCol = Coordinate::columnIndexFromString($imageTL[0]);
$imageTL = Coordinate::indexesFromString($drawing->getCoordinates());
$this->sheetDrawings[$drawing->getCoordinates()] = $drawing;
if ($imageTL[1] > $rowMax) {
$rowMax = $imageTL[1];
}
if ($imageCol > Coordinate::columnIndexFromString($colMax)) {
if ($imageTL[0] > $colMax) {
$colMax = $imageTL[0];
}
}
// Don't extend rows if not needed
if ($row === $rowMax || !$anyfound) {
return '';
}
$html = '';
++$colMax;
++$row;
while ($row <= $rowMax) {
$html .= '<tr>';
for ($col = 'A'; $col != $colMax; ++$col) {
$htmlx = $this->writeImageInCell($worksheet, $col . $row);
$htmlx .= $this->includeCharts ? $this->writeChartInCell($worksheet, $col . $row) : '';
if ($htmlx) {
$html .= "<td class='style0' style='position: relative;'>$htmlx</td>";
} else {
$html .= "<td class='style0'></td>";
}
}
++$row;
$html .= '</tr>' . PHP_EOL;
}
return $html;
}
/**
@@ -658,19 +621,16 @@ class Html extends BaseWriter
/**
* Generate image tag in cell.
*
* @param Worksheet $worksheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
* @param string $coordinates Cell coordinates
*/
private function writeImageInCell(Worksheet $worksheet, string $coordinates): string
private function writeImageInCell(string $coordinates): string
{
// Construct HTML
$html = '';
// Write images
foreach ($worksheet->getDrawingCollection() as $drawing) {
if ($drawing->getCoordinates() != $coordinates) {
continue;
}
$drawing = $this->sheetDrawings[$coordinates] ?? null;
if ($drawing !== null) {
$filedesc = $drawing->getDescription();
$filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded image';
if ($drawing instanceof Drawing) {
@@ -744,37 +704,47 @@ class Html extends BaseWriter
$html = '';
// Write charts
foreach ($worksheet->getChartCollection() as $chart) {
if ($chart instanceof Chart) {
$chartCoordinates = $chart->getTopLeftPosition();
if ($chartCoordinates['cell'] == $coordinates) {
$chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png';
$renderedWidth = $chart->getRenderedWidth();
$renderedHeight = $chart->getRenderedHeight();
if ($renderedWidth === null || $renderedHeight === null) {
$this->adjustRendererPositions($chart, $worksheet);
}
$renderSuccessful = $chart->render($chartFileName);
$chart->setRenderedWidth($renderedWidth);
$chart->setRenderedHeight($renderedHeight);
if (!$renderSuccessful) {
return '';
}
$html .= PHP_EOL;
$imageDetails = getimagesize($chartFileName) ?: ['', '', 'mime' => ''];
$filedesc = $chart->getTitle();
$filedesc = $filedesc ? $filedesc->getCaptionText() : '';
$filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart';
$picture = file_get_contents($chartFileName);
if ($picture !== false) {
$base64 = base64_encode($picture);
$imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64;
$html .= '<img style="position: absolute; z-index: 1; left: ' . $chartCoordinates['xOffset'] . 'px; top: ' . $chartCoordinates['yOffset'] . 'px; width: ' . $imageDetails[0] . 'px; height: ' . $imageDetails[1] . 'px;" src="' . $imageData . '" alt="' . $filedesc . '" />' . PHP_EOL;
}
unlink($chartFileName);
$chart = $this->sheetCharts[$coordinates] ?? null;
if ($chart !== null) {
$chartCoordinates = $chart->getTopLeftPosition();
$chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png';
$renderedWidth = $chart->getRenderedWidth();
$renderedHeight = $chart->getRenderedHeight();
if ($renderedWidth === null || $renderedHeight === null) {
$this->adjustRendererPositions($chart, $worksheet);
}
$title = $chart->getTitle();
$caption = null;
$filedesc = '';
if ($title !== null) {
$calculatedTitle = $title->getCalculatedTitle($worksheet->getParent());
if ($calculatedTitle !== null) {
$caption = $title->getCaption();
$title->setCaption($calculatedTitle);
}
$filedesc = $title->getCaptionText($worksheet->getParent());
}
$renderSuccessful = $chart->render($chartFileName);
$chart->setRenderedWidth($renderedWidth);
$chart->setRenderedHeight($renderedHeight);
if (isset($title, $caption)) {
$title->setCaption($caption);
}
if (!$renderSuccessful) {
return '';
}
$html .= PHP_EOL;
$imageDetails = getimagesize($chartFileName) ?: ['', '', 'mime' => ''];
$filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart';
$picture = file_get_contents($chartFileName);
unlink($chartFileName);
if ($picture !== false) {
$base64 = base64_encode($picture);
$imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64;
$html .= '<img style="position: absolute; z-index: 1; left: ' . $chartCoordinates['xOffset'] . 'px; top: ' . $chartCoordinates['yOffset'] . 'px; width: ' . $imageDetails[0] . 'px; height: ' . $imageDetails[1] . 'px;" src="' . $imageData . '" alt="' . $filedesc . '" />' . PHP_EOL;
}
}
@@ -1444,7 +1414,7 @@ class Html extends BaseWriter
private function generateRowWriteCell(string &$html, Worksheet $worksheet, string $coordinate, string $cellType, string $cellData, int $colSpan, int $rowSpan, $cssClass, int $colNum, int $sheetIndex, int $row): void
{
// Image?
$htmlx = $this->writeImageInCell($worksheet, $coordinate);
$htmlx = $this->writeImageInCell($coordinate);
// Chart?
$htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate);
// Column start
+66 -3
View File
@@ -99,9 +99,11 @@ class Chart extends WriterPart
$objWriter->writeAttribute('val', (string) (int) $chart->getPlotVisibleOnly());
$objWriter->endElement();
$objWriter->startElement('c:dispBlanksAs');
$objWriter->writeAttribute('val', $chart->getDisplayBlanksAs());
$objWriter->endElement();
if ($chart->getDisplayBlanksAs() !== '') {
$objWriter->startElement('c:dispBlanksAs');
$objWriter->writeAttribute('val', $chart->getDisplayBlanksAs());
$objWriter->endElement();
}
$objWriter->startElement('c:showDLblsOverMax');
$objWriter->writeAttribute('val', '0');
@@ -151,6 +153,9 @@ class Chart extends WriterPart
if ($title === null) {
return;
}
if ($this->writeCalculatedTitle($objWriter, $title)) {
return;
}
$objWriter->startElement('c:title');
$objWriter->startElement('c:tx');
@@ -187,6 +192,60 @@ class Chart extends WriterPart
$objWriter->endElement();
}
/**
* Write Calculated Chart Title.
*/
private function writeCalculatedTitle(XMLWriter $objWriter, Title $title): bool
{
$calc = $title->getCalculatedTitle($this->getParentWriter()->getSpreadsheet());
if (empty($calc)) {
return false;
}
$objWriter->startElement('c:title');
$objWriter->startElement('c:tx');
$objWriter->startElement('c:strRef');
$objWriter->writeElement('c:f', $title->getCellReference());
$objWriter->startElement('c:strCache');
$objWriter->startElement('c:ptCount');
$objWriter->writeAttribute('val', '1');
$objWriter->endElement(); // c:ptCount
$objWriter->startElement('c:pt');
$objWriter->writeAttribute('idx', '0');
$objWriter->writeElement('c:v', $calc);
$objWriter->endElement(); // c:pt
$objWriter->endElement(); // c:strCache
$objWriter->endElement(); // c:strRef
$objWriter->endElement(); // c:tx
$this->writeLayout($objWriter, $title->getLayout());
$objWriter->startElement('c:overlay');
$objWriter->writeAttribute('val', ($title->getOverlay()) ? '1' : '0');
$objWriter->endElement(); // c:overlay
// c:spPr
// c:txPr
$labelFont = $title->getFont();
if ($labelFont !== null) {
$objWriter->startElement('c:txPr');
$objWriter->startElement('a:bodyPr');
$objWriter->endElement(); // a:bodyPr
$objWriter->startElement('a:lstStyle');
$objWriter->endElement(); // a:lstStyle
$this->writeLabelFont($objWriter, $labelFont, null);
$objWriter->endElement(); // c:txPr
}
$objWriter->endElement(); // c:title
return true;
}
/**
* Write Chart Legend.
*/
@@ -1804,6 +1863,10 @@ class Chart extends WriterPart
if ($labelFont->getItalic() === true) {
$objWriter->writeAttribute('i', '1');
}
$cap = $labelFont->getCap();
if ($cap !== null) {
$objWriter->writeAttribute('cap', $cap);
}
$fontColor = $labelFont->getChartColor();
if ($fontColor !== null) {
$this->writeColor($objWriter, $fontColor);
@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Chart;
use PhpOffice\PhpSpreadsheet\Chart\Chart;
use PhpOffice\PhpSpreadsheet\Chart\DataSeries;
use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues;
use PhpOffice\PhpSpreadsheet\Chart\PlotArea;
use PhpOffice\PhpSpreadsheet\Chart\Title;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
use PhpOffice\PhpSpreadsheet\Settings;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Font;
use PhpOffice\PhpSpreadsheet\Writer\Html;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
class ChartsDynamicTitleTest extends AbstractFunctional
{
protected function tearDown(): void
{
Settings::unsetChartRenderer();
}
public function readCharts(XlsxReader $reader): void
{
$reader->setIncludeCharts(true);
}
public function writeCharts(XlsxWriter $writer): void
{
$writer->setIncludeCharts(true);
}
public function testDynamicTitle(): void
{
// based on samples/templates/issue.3797.2007.xlsx
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('Only Sheet');
$sheet->fromArray(
[
['Some Title'],
[],
[null, null, 'Data'],
[null, 'L1', 1.3],
[null, 'L2', 1.3],
[null, 'L3', 2.3],
[null, 'L4', 1.6],
[null, 'L5', 1.5],
[null, 'L6', 1.4],
[null, 'L7', 2.2],
[null, 'L8', 1.8],
[null, 'L9', 1.1],
[null, 'L10', 1.8],
[null, 'L11', 1.6],
[null, 'L12', 2.7],
[null, 'L13', 2.2],
[null, 'L14', 1.3],
]
);
$dataSeriesLabels = [
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, '\'Only Sheet\'!$B$4', null, 1), // 2010
];
// Set the X-Axis Labels
$xAxisTickValues = [
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, '\'Only Sheet\'!$B$4:$B$17'),
];
// Set the Data values for each data series we want to plot
$dataSeriesValues = [
new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, '\'Only Sheet\'!$C$4:$C$17'),
];
// Build the dataseries
$series = new DataSeries(
DataSeries::TYPE_BARCHART, // plotType
DataSeries::GROUPING_STANDARD, // 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]);
$title = new Title();
$title->setCellReference('\'Only Sheet\'!$A$1');
$font = new Font();
$font->setCap(Font::CAP_ALL);
$title->setFont($font);
// Create the chart
$chart = new Chart(
'chart1', // name
$title, // title
null, // legend
$plotArea, // plotArea
true, // plotVisibleOnly
DataSeries::EMPTY_AS_GAP, // displayBlanksAs
null, // xAxisLabel
null, // yAxisLabel
null, // xAxis
);
// Set the position where the chart should appear in the worksheet
$chart->setTopLeftPosition('G7');
$chart->setBottomRightPosition('N21');
// Add the chart to the worksheet
$sheet->addChart($chart);
$sheet->setSelectedCells('D1');
/** @var callable */
$callableReader = [$this, 'readCharts'];
/** @var callable */
$callableWriter = [$this, 'writeCharts'];
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter);
$spreadsheet->disconnectWorksheets();
$rsheet = $reloadedSpreadsheet->getActiveSheet();
$charts2 = $rsheet->getChartCollection();
self::assertCount(1, $charts2);
$chart2 = $charts2[0];
self::assertNotNull($chart2);
$original = $chart2->getTitle()?->getCalculatedTitle($reloadedSpreadsheet);
self::assertSame('Some Title', $original);
$rsheet->getCell('A1')->setValue('Changed Title');
self::assertNotNull($chart2->getTitle());
self::assertSame('Changed Title', $chart2->getTitle()->getCalculatedTitle($reloadedSpreadsheet));
self::assertSame(Font::CAP_ALL, $chart2->getTitle()->getFont()?->getCap());
$writer = new Html($reloadedSpreadsheet);
Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class);
$writer->setIncludeCharts(true);
$content = $writer->generateHtmlAll();
self::assertStringContainsString('alt="Changed Title"', $content);
$reloadedSpreadsheet->disconnectWorksheets();
}
}