Skip to content

Commit

Permalink
Chart Dynamic Title and Special Font Properties (#3800)
Browse files Browse the repository at this point in the history
* 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".
  • Loading branch information
oleibman authored Nov 30, 2023
1 parent 7fbf5c4 commit 009e009
Show file tree
Hide file tree
Showing 11 changed files with 480 additions and 99 deletions.
2 changes: 1 addition & 1 deletion samples/Chart/32_Chart_read_write.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
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';
}
Expand Down
83 changes: 83 additions & 0 deletions samples/Chart/37_Chart_dynamic_title.php
Original file line number Diff line number Diff line change
@@ -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 added samples/templates/37dynamictitle.xlsx
Binary file not shown.
58 changes: 56 additions & 2 deletions src/PhpSpreadsheet/Chart/Title.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -25,6 +32,10 @@ class Title
*/
private ?Layout $layout;

private string $cellReference = '';

private ?Font $font = null;

/**
* Create a new Title.
*
Expand All @@ -48,8 +59,14 @@ public function getCaption()
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;
Expand Down Expand Up @@ -100,13 +117,50 @@ public function getOverlay()
*
* @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;
}
}
19 changes: 17 additions & 2 deletions src/PhpSpreadsheet/Helper/Sample.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,25 +198,40 @@ public function log(string $message): void
*
* @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;
}
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
Expand Down
21 changes: 20 additions & 1 deletion src/PhpSpreadsheet/Reader/Xlsx/Chart.php
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@ private function chartTitle(SimpleXMLElement $titleDetails): Title
$caption = [];
$titleLayout = null;
$titleOverlay = false;
$titleFormula = null;
$titleFont = null;
foreach ($titleDetails as $titleDetailKey => $chartDetail) {
$chartDetail = Xlsx::testSimpleXml($chartDetail);
switch ($titleDetailKey) {
Expand All @@ -517,6 +519,9 @@ private function chartTitle(SimpleXMLElement $titleDetails): Title
$caption[] = (string) $pt->v;
}
}
if (isset($chartDetail->strRef->f)) {
$titleFormula = (string) $chartDetail->strRef->f;
}
}

break;
Expand All @@ -527,11 +532,24 @@ private function chartTitle(SimpleXMLElement $titleDetails): Title
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
Expand Down Expand Up @@ -1185,6 +1203,7 @@ private function parseFont(SimpleXMLElement $titleDetailPart): ?Font
$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');
Expand Down
5 changes: 5 additions & 0 deletions src/PhpSpreadsheet/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ public static function setChartRenderer(string $rendererClassName): void
self::$chartRenderer = $rendererClassName;
}

public static function unsetChartRenderer(): void
{
self::$chartRenderer = null;
}

/**
* Return the Chart Rendering Library that PhpSpreadsheet is currently configured to use.
*
Expand Down
31 changes: 31 additions & 0 deletions src/PhpSpreadsheet/Style/Font.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -236,6 +243,9 @@ public function applyFromArray(array $styleArray): static
if (isset($styleArray['scheme'])) {
$this->setScheme($styleArray['scheme']);
}
if (isset($styleArray['cap'])) {
$this->setCap($styleArray['cap']);
}
}

return $this;
Expand Down Expand Up @@ -795,6 +805,7 @@ public function getHashCode()
$this->hashChartColor($this->chartColor),
$this->hashChartColor($this->underlineColor),
(string) $this->baseLine,
(string) $this->cap,
]
)
. __CLASS__
Expand All @@ -806,6 +817,7 @@ protected function exportArray1(): array
$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());
Expand Down Expand Up @@ -847,4 +859,23 @@ public function setScheme(string $scheme): self

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;
}
}
Loading

0 comments on commit 009e009

Please sign in to comment.