mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-08-18 14:21:29 +00:00
Avoid Hard-Coding Some Chart Xml Attributes
Writer/Xlsx/Chart hard-codes a lot of output values. While nobody has reported a problem involving these, it makes sense to me to copy those values over from what Xlsx/Reader/Chart read, rather than hard-coding them. This involves adding some new properties to Chart and Layout, all, naturally, initialized to the values that we have been hard-coding. - Chart\date1904 - Chart\lang - Chart\pageMargins - Chart\pageSetup - Layout\bodyPr With one exception, I have not investigated any of the new properties in depth. That may come in time. The one I did look at is `date1904`. Chances are that it should match the equivalent spreadsheet setting, but it won't matter if there are no dates on your chart, and it often won't matter even if you do. It may well matter if you are using a `date axis`. While looking into that, it became apparent that some Shared Date conversions need to be a bit more flexible, specifying an optional `calendar` parameter rather than relying on the Spreadsheet `calendar` (which is not accessible while processing the chart) or the Shared\Date `calendar` (which is accessible).
This commit is contained in:
@@ -10,6 +10,10 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
require __DIR__ . '/../Header.php';
|
||||
/** @var \PhpOffice\PhpSpreadsheet\Helper\Sample $helper */
|
||||
$spreadsheet = new Spreadsheet();
|
||||
// following stmt can be commented in/out to test both calendars
|
||||
$spreadsheet->setExcelCalendar(SharedDate::CALENDAR_MAC_1904);
|
||||
$calendar = $spreadsheet->getExcelCalendar();
|
||||
$use1904 = $calendar === SharedDate::CALENDAR_MAC_1904;
|
||||
$dataSheet = $spreadsheet->getActiveSheet();
|
||||
$dataSheet->setTitle('Data');
|
||||
// changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date
|
||||
@@ -185,6 +189,9 @@ $chart = new Chart(
|
||||
$xAxis, // xAxis
|
||||
$yAxis, // yAxis
|
||||
);
|
||||
if ($use1904) {
|
||||
$chart->setDate1904(true);
|
||||
}
|
||||
|
||||
// Set the position of the chart in the chart sheet
|
||||
$chart->setTopLeftPosition('A1');
|
||||
@@ -275,7 +282,7 @@ $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601);
|
||||
$xAxis->setAxisType('dateAx'); // dateAx available ONLY for LINECHART, not SCATTERCHART
|
||||
|
||||
// measure the time span in Quarters, of data.
|
||||
$dateMinMax = dateRange(8, $spreadsheet); // array 'min'=>earliest date of first Q, 'max'=>latest date of final Q
|
||||
$dateMinMax = dateRange(8, $spreadsheet, $calendar); // array 'min'=>earliest date of first Q, 'max'=>latest date of final Q
|
||||
// change xAxis tick marks to match Qtr boundaries
|
||||
|
||||
$nQtrs = sprintf('%3.2f', (($dateMinMax['max'] - $dateMinMax['min']) / 30.5) / 4);
|
||||
@@ -321,6 +328,9 @@ $chart = new Chart(
|
||||
$xAxis, // xAxis
|
||||
$yAxis, // yAxis
|
||||
);
|
||||
if ($use1904) {
|
||||
$chart->setDate1904(true);
|
||||
}
|
||||
|
||||
// Set the position of the chart in the chart sheet below the first chart
|
||||
$chart->setTopLeftPosition('A13');
|
||||
@@ -339,7 +349,7 @@ $helper->write($spreadsheet, __FILE__, ['Xlsx'], true, resetActiveSheet: false);
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
|
||||
/** @return array{'min': float|int, 'max': float|int} */
|
||||
function dateRange(int $nrows, Spreadsheet $wrkbk): array
|
||||
function dateRange(int $nrows, Spreadsheet $wrkbk, int $calendar): array
|
||||
{
|
||||
$dataSheet = $wrkbk->getSheetByNameOrThrow('Data');
|
||||
|
||||
@@ -357,7 +367,7 @@ function dateRange(int $nrows, Spreadsheet $wrkbk): array
|
||||
$qtr = intdiv($startMonth, 3) + (($startMonth % 3 > 0) ? 1 : 0);
|
||||
$qtrStartMonth = sprintf('%02d', 1 + (($qtr - 1) * 3));
|
||||
$qtrStartStr = "$startYr-$qtrStartMonth-01";
|
||||
$ExcelQtrStartDateVal = SharedDate::convertIsoDate($qtrStartStr);
|
||||
$ExcelQtrStartDateVal = SharedDate::convertIsoDate($qtrStartStr, $calendar);
|
||||
|
||||
// end the xaxis at the end of the quarter of the last date
|
||||
/** @var string */
|
||||
@@ -377,7 +387,7 @@ function dateRange(int $nrows, Spreadsheet $wrkbk): array
|
||||
}
|
||||
$lastDOM = $lastDOMDate->format('t');
|
||||
$qtrEndStr = "$lastYr-$qtrEndMonth-$lastDOM";
|
||||
$ExcelQtrEndDateVal = SharedDate::convertIsoDate($qtrEndStr);
|
||||
$ExcelQtrEndDateVal = SharedDate::convertIsoDate($qtrEndStr, $calendar);
|
||||
|
||||
$minMaxDates = ['min' => $ExcelQtrStartDateVal, 'max' => $ExcelQtrEndDateVal];
|
||||
|
||||
|
||||
@@ -191,12 +191,22 @@ class Cell implements Stringable
|
||||
public function getFormattedValue(): string
|
||||
{
|
||||
$currentCalendar = SharedDate::getExcelCalendar();
|
||||
SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
|
||||
$formattedValue = (string) NumberFormat::toFormattedString(
|
||||
$this->getCalculatedValueString(),
|
||||
(string) $this->getStyle()->getNumberFormat()->getFormatCode(true)
|
||||
SharedDate::setExcelCalendar(
|
||||
$this->getWorksheet()
|
||||
->getParent()
|
||||
?->getExcelCalendar()
|
||||
);
|
||||
SharedDate::setExcelCalendar($currentCalendar);
|
||||
|
||||
try {
|
||||
$formattedValue = NumberFormat::toFormattedString(
|
||||
$this->getCalculatedValueString(),
|
||||
(string) $this->getStyle()
|
||||
->getNumberFormat()
|
||||
->getFormatCode(true)
|
||||
);
|
||||
} finally {
|
||||
SharedDate::setExcelCalendar($currentCalendar);
|
||||
}
|
||||
|
||||
return $formattedValue;
|
||||
}
|
||||
|
||||
@@ -110,6 +110,37 @@ class Chart
|
||||
|
||||
private bool $roundedCorners = false;
|
||||
|
||||
private bool $date1904 = false;
|
||||
|
||||
private string $lang = 'en-GB';
|
||||
|
||||
/** @var array{
|
||||
* b?: numeric-string,
|
||||
* l?: numeric-string,
|
||||
* r?: numeric-string,
|
||||
* t?: numeric-string,
|
||||
* header?: numeric-string,
|
||||
* footer?: numeric-string,
|
||||
* }
|
||||
*/
|
||||
private array $pageMargins = [
|
||||
'b' => '0.75',
|
||||
'l' => '0.7',
|
||||
'r' => '0.7',
|
||||
't' => '0.75',
|
||||
'header' => '0.3',
|
||||
'footer' => '0.3',
|
||||
];
|
||||
|
||||
/** @var array{
|
||||
* paperSize?: string,
|
||||
* orientation?: string,
|
||||
* }
|
||||
*/
|
||||
private array $pageSetup = [
|
||||
'orientation' => 'portrait',
|
||||
];
|
||||
|
||||
private GridLines $borderLines;
|
||||
|
||||
private ChartColor $fillColor;
|
||||
@@ -680,9 +711,11 @@ class Chart
|
||||
return $this->autoTitleDeleted;
|
||||
}
|
||||
|
||||
public function setAutoTitleDeleted(bool $autoTitleDeleted): self
|
||||
public function setAutoTitleDeleted(?bool $autoTitleDeleted): self
|
||||
{
|
||||
$this->autoTitleDeleted = $autoTitleDeleted;
|
||||
if (is_bool($autoTitleDeleted)) {
|
||||
$this->autoTitleDeleted = $autoTitleDeleted;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -725,6 +758,34 @@ class Chart
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDate1904(): bool
|
||||
{
|
||||
return $this->date1904;
|
||||
}
|
||||
|
||||
public function setDate1904(?bool $date1904): self
|
||||
{
|
||||
if ($date1904 !== null) {
|
||||
$this->date1904 = $date1904;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLang(): string
|
||||
{
|
||||
return $this->lang;
|
||||
}
|
||||
|
||||
public function setLang(?string $lang): self
|
||||
{
|
||||
if ($lang !== null && $lang !== '') {
|
||||
$this->lang = $lang;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBorderLines(): GridLines
|
||||
{
|
||||
return $this->borderLines;
|
||||
@@ -782,4 +843,58 @@ class Chart
|
||||
$this->borderLines = clone $this->borderLines;
|
||||
$this->fillColor = clone $this->fillColor;
|
||||
}
|
||||
|
||||
/** @return array{
|
||||
* b?: numeric-string,
|
||||
* l?: numeric-string,
|
||||
* r?: numeric-string,
|
||||
* t?: numeric-string,
|
||||
* header?: numeric-string,
|
||||
* footer?: numeric-string,
|
||||
* }
|
||||
*/
|
||||
public function getPageMargins(): array
|
||||
{
|
||||
return $this->pageMargins;
|
||||
}
|
||||
|
||||
/** @param mixed $pageMargins expecting array matching $this->pageMargins */
|
||||
public function setPageMargins(mixed $pageMargins): self
|
||||
{
|
||||
if (is_array($pageMargins)) {
|
||||
foreach (['b', 'l', 'r', 't', 'header', 'footer'] as $key) {
|
||||
$value = $pageMargins[$key] ?? null;
|
||||
if (is_string($value) && is_numeric($value)) {
|
||||
$this->pageMargins[$key] = "$value";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return array{
|
||||
* paperSize?: string,
|
||||
* orientation?: string,
|
||||
* }
|
||||
*/
|
||||
public function getPageSetup(): array
|
||||
{
|
||||
return $this->pageSetup;
|
||||
}
|
||||
|
||||
/** @param mixed $pageSetup expecting array matching $this->pageSetup */
|
||||
public function setPageSetup(mixed $pageSetup): self
|
||||
{
|
||||
if (is_array($pageSetup)) {
|
||||
foreach (['paperSize', 'orientation'] as $key) {
|
||||
$value = $pageSetup[$key] ?? null;
|
||||
if (is_string($value)) {
|
||||
$this->pageSetup[$key] = "$value";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,26 @@ class Layout
|
||||
|
||||
private ?Properties $labelEffects = null;
|
||||
|
||||
/** @var array{
|
||||
* vertOverflow?: string,
|
||||
* horzOverflow?: string,
|
||||
* wrap?: string,
|
||||
* lIns?: numeric-string,
|
||||
* tIns?: numeric-string,
|
||||
* rIns?: numeric-string,
|
||||
* bIns?: numeric-string,
|
||||
* anchor?: string,
|
||||
* }
|
||||
*/
|
||||
private array $bodyPr = [
|
||||
'wrap' => 'square',
|
||||
'lIns' => '38100',
|
||||
'tIns' => '19050',
|
||||
'rIns' => '38100',
|
||||
'bIns' => '19050',
|
||||
'anchor' => 'ctr',
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a new Layout.
|
||||
*
|
||||
@@ -106,7 +126,22 @@ class Layout
|
||||
*/
|
||||
public function __construct(array $layout = [])
|
||||
{
|
||||
/** @var array{layoutTarget?: string, xMode?: string, yMode?: string, x?: float, y?: float, w?:float, h?:float, dLblPos?: string, labelFont?: ?mixed, labelFontColor?: ?mixed, labelEffects?: ?mixed, numFmtCode?: string} $layout */
|
||||
/** @var array{
|
||||
* layoutTarget?: string,
|
||||
* xMode?: string,
|
||||
* yMode?: string,
|
||||
* x?: float,
|
||||
* y?: float,
|
||||
* w?:float,
|
||||
* h?:float,
|
||||
* dLblPos?: string,
|
||||
* labelFont?: ?mixed,
|
||||
* labelFontColor?: ?mixed,
|
||||
* labelEffects?: ?mixed,
|
||||
* numFmtCode?: string,
|
||||
* bodyPr?: mixed,
|
||||
* } $layout
|
||||
*/
|
||||
if (isset($layout['layoutTarget'])) {
|
||||
$this->layoutTarget = $layout['layoutTarget'];
|
||||
}
|
||||
@@ -156,6 +191,10 @@ class Layout
|
||||
if ($labelEffects instanceof Properties) {
|
||||
$this->labelEffects = $labelEffects;
|
||||
}
|
||||
$bodyPr = $layout['bodyPr'] ?? null;
|
||||
if (is_array($bodyPr)) {
|
||||
$this->setBodyPr($bodyPr);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param mixed[] $layout */
|
||||
@@ -523,6 +562,45 @@ class Layout
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return array{
|
||||
* vertOverflow?: string,
|
||||
* horzOverflow?: string,
|
||||
* wrap?: string,
|
||||
* lIns?: numeric-string,
|
||||
* tIns?: numeric-string,
|
||||
* rIns?: numeric-string,
|
||||
* bIns?: numeric-string,
|
||||
* anchor?: string,
|
||||
* }
|
||||
*/
|
||||
public function getBodyPr(): array
|
||||
{
|
||||
return $this->bodyPr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $bodyPr expect array matching $this->bodyPr
|
||||
*/
|
||||
public function setBodyPr(mixed $bodyPr): self
|
||||
{
|
||||
if (is_array($bodyPr)) {
|
||||
foreach (['vertOverflow', 'horzOverflow', 'wrap', 'anchor'] as $key) {
|
||||
$value = $bodyPr[$key] ?? null;
|
||||
if (is_string($value)) {
|
||||
$this->bodyPr[$key] = "$value";
|
||||
}
|
||||
}
|
||||
foreach (['lIns', 'tIns', 'rIns', 'bIns'] as $key) {
|
||||
$value = $bodyPr[$key] ?? null;
|
||||
if (is_string($value) && is_numeric($value)) {
|
||||
$this->bodyPr[$key] = "$value";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement PHP __clone to create a deep clone, not just a shallow copy.
|
||||
*/
|
||||
|
||||
@@ -92,13 +92,26 @@ class Chart
|
||||
$chartFillColor = null;
|
||||
$gradientArray = [];
|
||||
$gradientLin = null;
|
||||
$roundedCorners = false;
|
||||
$roundedCorners = null;
|
||||
$date1904 = null;
|
||||
$lang = null;
|
||||
$gapWidth = null;
|
||||
$useUpBars = null;
|
||||
$useDownBars = null;
|
||||
$noBorder = false;
|
||||
$pageMargins = [];
|
||||
$pageSetup = [];
|
||||
foreach ($chartElementsC as $chartElementKey => $chartElement) {
|
||||
switch ($chartElementKey) {
|
||||
case 'printSettings':
|
||||
if (isset($chartElement->pageMargins)) {
|
||||
$pageMargins = current((array) $chartElement->pageMargins->attributes());
|
||||
}
|
||||
if (isset($chartElement->pageSetup)) {
|
||||
$pageSetup = current((array) $chartElement->pageSetup->attributes());
|
||||
}
|
||||
|
||||
break;
|
||||
case 'spPr':
|
||||
$children = $chartElementsC->spPr->children($this->aNamespace);
|
||||
if (isset($children->noFill)) {
|
||||
@@ -117,16 +130,22 @@ class Chart
|
||||
|
||||
break;
|
||||
case 'roundedCorners':
|
||||
/** @var bool $roundedCorners */
|
||||
$roundedCorners = self::getAttributeBoolean($chartElementsC->roundedCorners, 'val');
|
||||
|
||||
break;
|
||||
case 'date1904':
|
||||
$date1904 = self::getAttributeBoolean($chartElementsC->date1904, 'val');
|
||||
|
||||
break;
|
||||
case 'lang':
|
||||
$lang = self::getAttributeString($chartElementsC->lang, 'val');
|
||||
|
||||
break;
|
||||
case 'chart':
|
||||
foreach ($chartElement as $chartDetailsKey => $chartDetails) {
|
||||
$chartDetails = Xlsx::testSimpleXml($chartDetails);
|
||||
switch ($chartDetailsKey) {
|
||||
case 'autoTitleDeleted':
|
||||
/** @var bool $autoTitleDeleted */
|
||||
$autoTitleDeleted = self::getAttributeBoolean($chartElementsC->chart->autoTitleDeleted, 'val');
|
||||
|
||||
break;
|
||||
@@ -482,23 +501,18 @@ class Chart
|
||||
if ($chartBorderLines !== null) {
|
||||
$chart->setBorderLines($chartBorderLines);
|
||||
}
|
||||
$chart->setNoBorder($noBorder);
|
||||
$chart->setRoundedCorners($roundedCorners);
|
||||
if (is_bool($autoTitleDeleted)) {
|
||||
$chart->setAutoTitleDeleted($autoTitleDeleted);
|
||||
}
|
||||
if (is_int($rotX)) {
|
||||
$chart->setRotX($rotX);
|
||||
}
|
||||
if (is_int($rotY)) {
|
||||
$chart->setRotY($rotY);
|
||||
}
|
||||
if (is_int($rAngAx)) {
|
||||
$chart->setRAngAx($rAngAx);
|
||||
}
|
||||
if (is_int($perspective)) {
|
||||
$chart->setPerspective($perspective);
|
||||
}
|
||||
$chart
|
||||
->setNoBorder($noBorder)
|
||||
->setRoundedCorners($roundedCorners)
|
||||
->setDate1904($date1904)
|
||||
->setLang($lang)
|
||||
->setPageMargins($pageMargins)
|
||||
->setPageSetup($pageSetup)
|
||||
->setAutoTitleDeleted($autoTitleDeleted)
|
||||
->setRotX($rotX)
|
||||
->setRotY($rotY)
|
||||
->setRAngAx($rAngAx)
|
||||
->setPerspective($perspective);
|
||||
|
||||
return $chart;
|
||||
}
|
||||
@@ -1299,6 +1313,9 @@ class Chart
|
||||
$plotAttributes['labelEffects'] = $labelEffects;
|
||||
}
|
||||
}
|
||||
if (isset($txpr->bodyPr)) {
|
||||
$plotAttributes['bodyPr'] = current((array) $txpr->bodyPr->attributes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1366,6 +1383,11 @@ class Chart
|
||||
/** @var ?Font $plotAttributeValue */
|
||||
$plotArea->setLabelFont($plotAttributeValue);
|
||||
|
||||
break;
|
||||
case 'bodyPr':
|
||||
/** @var mixed $plotAttributeValue */
|
||||
$plotArea->setBodyPr($plotAttributeValue);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ class Date
|
||||
* serialized timestamp.
|
||||
* See https://en.wikipedia.org/wiki/ISO_8601 for details of the ISO-8601 standard format.
|
||||
*/
|
||||
public static function convertIsoDate(mixed $value): float|int
|
||||
public static function convertIsoDate(mixed $value, ?int $calendar = null): float|int
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
throw new Exception('Non-string value supplied for Iso Date conversion');
|
||||
@@ -173,7 +173,7 @@ class Date
|
||||
throw new Exception("Invalid string $value supplied for datatype Date");
|
||||
}
|
||||
|
||||
$newValue = self::dateTimeToExcel($date);
|
||||
$newValue = self::dateTimeToExcel($date, $calendar);
|
||||
|
||||
if (preg_match('/^\s*\d?\d:\d\d(:\d\d([.]\d+)?)?\s*(am|pm)?\s*$/i', $value) == 1) {
|
||||
$newValue = fmod($newValue, 1.0);
|
||||
@@ -194,16 +194,17 @@ class Date
|
||||
*
|
||||
* @return DateTime PHP date/time object
|
||||
*/
|
||||
public static function excelToDateTimeObject(float|int $excelTimestamp, null|DateTimeZone|string $timeZone = null): DateTime
|
||||
public static function excelToDateTimeObject(float|int $excelTimestamp, null|DateTimeZone|string $timeZone = null, ?int $calendar = null): DateTime
|
||||
{
|
||||
$calendar ??= self::$excelCalendar;
|
||||
$timeZone = ($timeZone === null) ? self::getDefaultTimezone() : self::validateTimeZone($timeZone);
|
||||
if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) {
|
||||
if ($excelTimestamp < 1 && self::$excelCalendar === self::CALENDAR_WINDOWS_1900) {
|
||||
if ($excelTimestamp < 1 && $calendar === self::CALENDAR_WINDOWS_1900) {
|
||||
// Unix timestamp base date
|
||||
$baseDate = new DateTime('1970-01-01', $timeZone);
|
||||
} else {
|
||||
// MS Excel calendar base dates
|
||||
if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) {
|
||||
if ($calendar == self::CALENDAR_WINDOWS_1900) {
|
||||
// Allow adjustment for 1900 Leap Year in MS Excel
|
||||
$baseDate = ($excelTimestamp < 60) ? new DateTime('1899-12-31', $timeZone) : new DateTime('1899-12-30', $timeZone);
|
||||
} else {
|
||||
@@ -252,9 +253,9 @@ class Date
|
||||
*
|
||||
* @return int Unix timetamp for this date/time
|
||||
*/
|
||||
public static function excelToTimestamp($excelTimestamp, $timeZone = null): int
|
||||
public static function excelToTimestamp($excelTimestamp, $timeZone = null, ?int $calendar = null): int
|
||||
{
|
||||
$dto = self::excelToDateTimeObject($excelTimestamp, $timeZone);
|
||||
$dto = self::excelToDateTimeObject($excelTimestamp, $timeZone, $calendar);
|
||||
self::roundMicroseconds($dto);
|
||||
|
||||
return (int) $dto->format('U');
|
||||
@@ -269,14 +270,16 @@ class Date
|
||||
* @return false|float Excel date/time value
|
||||
* or boolean FALSE on failure
|
||||
*/
|
||||
public static function PHPToExcel(mixed $dateValue)
|
||||
public static function PHPToExcel(mixed $dateValue, ?int $calendar = null)
|
||||
{
|
||||
if ((is_object($dateValue)) && ($dateValue instanceof DateTimeInterface)) {
|
||||
return self::dateTimeToExcel($dateValue);
|
||||
} elseif (is_numeric($dateValue)) {
|
||||
return self::timestampToExcel($dateValue);
|
||||
} elseif (is_string($dateValue)) {
|
||||
return self::stringToExcel($dateValue);
|
||||
return self::dateTimeToExcel($dateValue, $calendar);
|
||||
}
|
||||
if (is_numeric($dateValue)) {
|
||||
return self::timestampToExcel($dateValue, $calendar);
|
||||
}
|
||||
if (is_string($dateValue)) {
|
||||
return self::stringToExcel($dateValue, $calendar);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -289,7 +292,7 @@ class Date
|
||||
*
|
||||
* @return float MS Excel serialized date/time value
|
||||
*/
|
||||
public static function dateTimeToExcel(DateTimeInterface $dateValue): float
|
||||
public static function dateTimeToExcel(DateTimeInterface $dateValue, ?int $calendar = null): float
|
||||
{
|
||||
$seconds = (float) sprintf('%d.%06d', $dateValue->format('s'), $dateValue->format('u'));
|
||||
|
||||
@@ -299,7 +302,8 @@ class Date
|
||||
(int) $dateValue->format('d'),
|
||||
(int) $dateValue->format('H'),
|
||||
(int) $dateValue->format('i'),
|
||||
$seconds
|
||||
$seconds,
|
||||
$calendar
|
||||
);
|
||||
}
|
||||
|
||||
@@ -312,13 +316,13 @@ class Date
|
||||
*
|
||||
* @return false|float MS Excel serialized date/time value
|
||||
*/
|
||||
public static function timestampToExcel($unixTimestamp): bool|float
|
||||
public static function timestampToExcel($unixTimestamp, ?int $calendar = null): bool|float
|
||||
{
|
||||
if (!is_numeric($unixTimestamp)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::dateTimeToExcel(new DateTime('@' . $unixTimestamp));
|
||||
return self::dateTimeToExcel(new DateTime('@' . $unixTimestamp), $calendar);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -326,9 +330,10 @@ class Date
|
||||
*
|
||||
* @return float Excel date/time value
|
||||
*/
|
||||
public static function formattedPHPToExcel(int $year, int $month, int $day, int $hours = 0, int $minutes = 0, float|int $seconds = 0): float
|
||||
public static function formattedPHPToExcel(int $year, int $month, int $day, int $hours = 0, int $minutes = 0, float|int $seconds = 0, ?int $calendar = null): float
|
||||
{
|
||||
if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) {
|
||||
$calendar ??= self::$excelCalendar;
|
||||
if ($calendar === self::CALENDAR_WINDOWS_1900) {
|
||||
//
|
||||
// Fudge factor for the erroneous fact that the year 1900 is treated as a Leap Year in MS Excel
|
||||
// This affects every date following 28th February 1900
|
||||
@@ -474,7 +479,7 @@ class Date
|
||||
*
|
||||
* @return false|float Excel date/time serial value
|
||||
*/
|
||||
public static function stringToExcel(string $dateValue): bool|float
|
||||
public static function stringToExcel(string $dateValue, ?int $calendar = null): bool|float
|
||||
{
|
||||
if (strlen($dateValue) < 2) {
|
||||
return false;
|
||||
@@ -483,7 +488,16 @@ class Date
|
||||
return false;
|
||||
}
|
||||
|
||||
$dateValueNew = DateTimeExcel\DateValue::fromString($dateValue);
|
||||
$hold = self::$excelCalendar;
|
||||
|
||||
try {
|
||||
if ($calendar !== null) {
|
||||
self::$excelCalendar = $calendar;
|
||||
}
|
||||
$dateValueNew = DateTimeExcel\DateValue::fromString($dateValue);
|
||||
} finally {
|
||||
self::$excelCalendar = $hold;
|
||||
}
|
||||
|
||||
if (!is_float($dateValueNew)) {
|
||||
return false;
|
||||
|
||||
@@ -53,10 +53,10 @@ class Chart extends WriterPart
|
||||
$objWriter->writeAttribute('xmlns:r', Namespaces::SCHEMA_OFFICE_DOCUMENT);
|
||||
|
||||
$objWriter->startElement('c:date1904');
|
||||
$objWriter->writeAttribute('val', '0');
|
||||
$objWriter->writeAttribute('val', $chart->getDate1904() ? '1' : '0');
|
||||
$objWriter->endElement();
|
||||
$objWriter->startElement('c:lang');
|
||||
$objWriter->writeAttribute('val', 'en-GB');
|
||||
$objWriter->writeAttribute('val', $chart->getLang());
|
||||
$objWriter->endElement();
|
||||
$objWriter->startElement('c:roundedCorners');
|
||||
$objWriter->writeAttribute('val', $chart->getRoundedCorners() ? '1' : '0');
|
||||
@@ -72,7 +72,6 @@ class Chart extends WriterPart
|
||||
$objWriter->writeAttribute('val', (string) (int) $chart->getAutoTitleDeleted());
|
||||
$objWriter->endElement();
|
||||
|
||||
$objWriter->startElement('c:view3D');
|
||||
$surface2D = false;
|
||||
$plotArea = $chart->getPlotArea();
|
||||
if ($plotArea !== null) {
|
||||
@@ -85,11 +84,14 @@ class Chart extends WriterPart
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->view3DStarted = false;
|
||||
$this->writeView3D($objWriter, $chart->getRotX(), 'c:rotX', $surface2D, 90);
|
||||
$this->writeView3D($objWriter, $chart->getRotY(), 'c:rotY', $surface2D);
|
||||
$this->writeView3D($objWriter, $chart->getRAngAx(), 'c:rAngAx', $surface2D);
|
||||
$this->writeView3D($objWriter, $chart->getPerspective(), 'c:perspective', $surface2D);
|
||||
$objWriter->endElement(); // view3D
|
||||
if ($this->view3DStarted) {
|
||||
$objWriter->endElement(); // view3D
|
||||
}
|
||||
|
||||
$this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY());
|
||||
|
||||
@@ -123,7 +125,7 @@ class Chart extends WriterPart
|
||||
$this->writeEffects($objWriter, $borderLines);
|
||||
$objWriter->endElement(); // c:spPr
|
||||
|
||||
$this->writePrintSettings($objWriter);
|
||||
$this->writePrintSettings($objWriter, $chart);
|
||||
|
||||
$objWriter->endElement(); // c:chartSpace
|
||||
|
||||
@@ -131,12 +133,18 @@ class Chart extends WriterPart
|
||||
return $objWriter->getData();
|
||||
}
|
||||
|
||||
private bool $view3DStarted = false;
|
||||
|
||||
private function writeView3D(XMLWriter $objWriter, ?int $value, string $tag, bool $surface2D, int $default = 0): void
|
||||
{
|
||||
if ($value === null && $surface2D) {
|
||||
$value = $default;
|
||||
}
|
||||
if ($value !== null) {
|
||||
if (!$this->view3DStarted) {
|
||||
$objWriter->startElement('c:view3D');
|
||||
$this->view3DStarted = true;
|
||||
}
|
||||
$objWriter->startElement($tag);
|
||||
$objWriter->writeAttribute('val', "$value");
|
||||
$objWriter->endElement();
|
||||
@@ -549,12 +557,12 @@ class Chart extends WriterPart
|
||||
$objWriter->startElement('c:txPr');
|
||||
|
||||
$objWriter->startElement('a:bodyPr');
|
||||
$objWriter->writeAttribute('wrap', 'square');
|
||||
$objWriter->writeAttribute('lIns', '38100');
|
||||
$objWriter->writeAttribute('tIns', '19050');
|
||||
$objWriter->writeAttribute('rIns', '38100');
|
||||
$objWriter->writeAttribute('bIns', '19050');
|
||||
$objWriter->writeAttribute('anchor', 'ctr');
|
||||
$bodyPr = $chartLayout->getBodyPr();
|
||||
foreach (['vertOverflow', 'horzOverflow', 'wrap', 'lIns', 'tIns', 'rIns', 'bIns', 'anchor'] as $key) {
|
||||
if (isset($bodyPr[$key])) {
|
||||
$objWriter->writeAttribute($key, $bodyPr[$key]);
|
||||
}
|
||||
}
|
||||
$objWriter->startElement('a:spAutoFit');
|
||||
$objWriter->endElement(); // a:spAutoFit
|
||||
$objWriter->endElement(); // a:bodyPr
|
||||
@@ -1716,27 +1724,32 @@ class Chart extends WriterPart
|
||||
/**
|
||||
* Write Printer Settings.
|
||||
*/
|
||||
private function writePrintSettings(XMLWriter $objWriter): void
|
||||
private function writePrintSettings(XMLWriter $objWriter, SpreadsheetChart $chart): void
|
||||
{
|
||||
$objWriter->startElement('c:printSettings');
|
||||
|
||||
$objWriter->startElement('c:headerFooter');
|
||||
$objWriter->endElement();
|
||||
|
||||
$pageMargins = $chart->getPageMargins();
|
||||
$objWriter->startElement('c:pageMargins');
|
||||
$objWriter->writeAttribute('footer', '0.3');
|
||||
$objWriter->writeAttribute('header', '0.3');
|
||||
$objWriter->writeAttribute('r', '0.7');
|
||||
$objWriter->writeAttribute('l', '0.7');
|
||||
$objWriter->writeAttribute('t', '0.75');
|
||||
$objWriter->writeAttribute('b', '0.75');
|
||||
$objWriter->endElement();
|
||||
foreach (['b', 'l', 'r', 't', 'header', 'footer'] as $key) {
|
||||
if (array_key_exists($key, $pageMargins)) {
|
||||
$objWriter->writeAttribute($key, $pageMargins[$key]);
|
||||
}
|
||||
}
|
||||
$objWriter->endElement(); // c:pageMargins
|
||||
|
||||
$pageSetup = $chart->getPageSetup();
|
||||
$objWriter->startElement('c:pageSetup');
|
||||
$objWriter->writeAttribute('orientation', 'portrait');
|
||||
$objWriter->endElement();
|
||||
foreach (['paperSize', 'orientation'] as $key) {
|
||||
if (array_key_exists($key, $pageSetup)) {
|
||||
$objWriter->writeAttribute($key, $pageSetup[$key]);
|
||||
}
|
||||
}
|
||||
$objWriter->endElement(); // c:pageSetup
|
||||
|
||||
$objWriter->endElement();
|
||||
$objWriter->endElement(); // c:printSettings
|
||||
}
|
||||
|
||||
private function writeEffects(XMLWriter $objWriter, Properties $yAxis): void
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Chart;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Chart\Chart;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class CopyXmlTest extends TestCase
|
||||
{
|
||||
public function testCopyXml(): void
|
||||
{
|
||||
$infile = 'samples/templates/32readwriteRadarChart4.xlsx';
|
||||
$reader = new XlsxReader();
|
||||
$reader->setIncludeCharts(true);
|
||||
$spreadsheet = $reader->load($infile);
|
||||
$sheet = $spreadsheet->getSheetByNameOrThrow('Charts');
|
||||
$charts = $sheet->getChartCollection();
|
||||
self::assertCount(1, $charts);
|
||||
$chart = $charts[0] ?? null;
|
||||
self::assertInstanceOf(Chart::class, $chart);
|
||||
|
||||
$writer = new XlsxWriter($spreadsheet);
|
||||
$writer->setIncludeCharts(true);
|
||||
$writer = new XlsxWriter($spreadsheet);
|
||||
$writer->setIncludeCharts(true);
|
||||
$writerChart = new XlsxWriter\Chart($writer);
|
||||
$data = $writerChart->writeChart($chart);
|
||||
//echo $data;
|
||||
self::assertStringContainsString('<c:date1904 val="0"/>', $data, 'From input even though same as default');
|
||||
self::assertStringContainsString('<c:lang val="en-US"/>', $data, 'From input, different from default');
|
||||
self::assertStringNotContainsString('view3D', $data, 'No empty view3D tag');
|
||||
self::assertStringContainsString('<a:bodyPr vertOverflow="overflow" horzOverflow="overflow" wrap="square" lIns="38100" tIns="19050" rIns="38100" bIns="19050" anchor="ctr">', $data, 'A couple of extra attributes');
|
||||
self::assertStringContainsString('<c:pageMargins b="0.75" l="0.25" r="0.25" t="0.75" header="0.3" footer="0.3"/>', $data, 'Some different values plus re-shuffling');
|
||||
self::assertStringContainsString('<c:pageSetup paperSize="9" orientation="portrait"/>', $data, 'An extra attribute');
|
||||
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ class Issue2931Test extends TestCase
|
||||
'<c:view3D><c:rotX val="90"/><c:rotY val="0"/><c:rAngAx val="0"/><c:perspective val="0"/></c:view3D>',
|
||||
];
|
||||
$expectedXml3D = [
|
||||
'<c:view3D/>',
|
||||
'c:view3D', // empty view3d no longer generated
|
||||
];
|
||||
$expectedXmlNoX = [
|
||||
'c:grouping',
|
||||
@@ -102,7 +102,7 @@ class Issue2931Test extends TestCase
|
||||
$data = $writerChart->writeChart($chart);
|
||||
// confirm that file contains expected tags
|
||||
foreach ($expectedXml3D as $expected) {
|
||||
self::assertSame(1, substr_count($data, $expected), $expected);
|
||||
self::assertSame(0, substr_count($data, $expected), $expected);
|
||||
}
|
||||
foreach ($expectedXmlNoX as $expected) {
|
||||
self::assertSame(0, substr_count($data, $expected), $expected);
|
||||
|
||||
@@ -100,6 +100,17 @@ class DateTest extends TestCase
|
||||
self::assertEqualsWithDelta($expectedResult, $result, 1E-5);
|
||||
}
|
||||
|
||||
#[DataProvider('providerDateTimeDateTimeToExcel')]
|
||||
public function testDateTime2DateTimeToExcel(float|int $expectedResult, DateTimeInterface $dateTimeObject): void
|
||||
{
|
||||
// Show new parameter will override static value
|
||||
Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
|
||||
|
||||
$result = Date::dateTimeToExcel($dateTimeObject, Date::CALENDAR_WINDOWS_1900);
|
||||
self::assertEqualsWithDelta($expectedResult, $result, 1E-5);
|
||||
self::assertSame(Date::CALENDAR_MAC_1904, Date::getExcelCalendar());
|
||||
}
|
||||
|
||||
public static function providerDateTimeDateTimeToExcel(): array
|
||||
{
|
||||
return require 'tests/data/Shared/Date/DateTimeToExcel.php';
|
||||
@@ -134,6 +145,18 @@ class DateTest extends TestCase
|
||||
self::assertEquals($expectedResult, $result);
|
||||
}
|
||||
|
||||
#[DataProvider('providerDateTimeExcelToTimestamp1904')]
|
||||
public function testDateTime2ExcelToTimestamp1904(float|int $expectedResult, float|int $excelDateTimeValue): void
|
||||
{
|
||||
if ($expectedResult > PHP_INT_MAX || $expectedResult < PHP_INT_MIN) {
|
||||
self::markTestSkipped('Test invalid on 32-bit system.');
|
||||
}
|
||||
|
||||
$result = Date::excelToTimestamp($excelDateTimeValue, calendar: Date::CALENDAR_MAC_1904);
|
||||
self::assertEquals($expectedResult, $result);
|
||||
self::assertSame($this->excelCalendar, Date::getExcelCalendar());
|
||||
}
|
||||
|
||||
public static function providerDateTimeExcelToTimestamp1904(): array
|
||||
{
|
||||
return require 'tests/data/Shared/Date/ExcelToTimestamp1904.php';
|
||||
@@ -148,6 +171,14 @@ class DateTest extends TestCase
|
||||
self::assertEqualsWithDelta($expectedResult, $result, 1E-5);
|
||||
}
|
||||
|
||||
#[DataProvider('providerDateTimeTimestampToExcel1904')]
|
||||
public function testDateTime2TimestampToExcel1904(mixed $expectedResult, float|int|string $unixTimestamp): void
|
||||
{
|
||||
$result = Date::timestampToExcel($unixTimestamp, Date::CALENDAR_MAC_1904);
|
||||
self::assertEqualsWithDelta($expectedResult, $result, 1E-5);
|
||||
self::assertSame($this->excelCalendar, Date::getExcelCalendar());
|
||||
}
|
||||
|
||||
public static function providerDateTimeTimestampToExcel1904(): array
|
||||
{
|
||||
return require 'tests/data/Shared/Date/TimestampToExcel1904.php';
|
||||
@@ -204,6 +235,9 @@ class DateTest extends TestCase
|
||||
self::assertNotFalse($timestamp2);
|
||||
self::assertEqualsWithDelta(45803.60277777778, $timestamp1, 1.0E-10);
|
||||
self::assertSame($timestamp1, $timestamp2);
|
||||
$timestamp3 = Date::stringToExcel('26.05.2025 14:28:00.00', Date::CALENDAR_MAC_1904);
|
||||
self::assertEqualsWithDelta(45803.60277777778, 1462 + $timestamp3, 1.0E-10);
|
||||
self::assertSame($this->excelCalendar, Date::getExcelCalendar());
|
||||
|
||||
$date = Date::PHPToExcel('2020-01-01');
|
||||
self::assertEquals(43831.0, $date);
|
||||
|
||||
Reference in New Issue
Block a user