mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-09-18 05:57:33 +00:00
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:
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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.
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user