This commit is contained in:
steven 2025-08-11 22:23:30 +02:00
commit 72a26edcff
22092 changed files with 2101903 additions and 0 deletions

View file

@ -0,0 +1,343 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart;
/**
* Created by PhpStorm.
* User: Wiktor Trzonkowski
* Date: 6/17/14
* Time: 12:11 PM.
*/
class Axis extends Properties
{
const AXIS_TYPE_CATEGORY = 'catAx';
const AXIS_TYPE_DATE = 'dateAx';
const AXIS_TYPE_VALUE = 'valAx';
const TIME_UNIT_DAYS = 'days';
const TIME_UNIT_MONTHS = 'months';
const TIME_UNIT_YEARS = 'years';
public function __construct()
{
parent::__construct();
$this->fillColor = new ChartColor();
}
/**
* Chart Major Gridlines as.
*/
private ?GridLines $majorGridlines = null;
/**
* Chart Minor Gridlines as.
*/
private ?GridLines $minorGridlines = null;
/**
* Axis Number.
*
* @var mixed[]
*/
private array $axisNumber = [
'format' => self::FORMAT_CODE_GENERAL,
'source_linked' => 1,
'numeric' => null,
];
private string $axisType = '';
private ?AxisText $axisText = null;
private ?Title $dispUnitsTitle = null;
/**
* Axis Options.
*
* @var array<string, null|string>
*/
private array $axisOptions = [
'minimum' => null,
'maximum' => null,
'major_unit' => null,
'minor_unit' => null,
'orientation' => self::ORIENTATION_NORMAL,
'minor_tick_mark' => self::TICK_MARK_NONE,
'major_tick_mark' => self::TICK_MARK_NONE,
'axis_labels' => self::AXIS_LABELS_NEXT_TO,
'horizontal_crosses' => self::HORIZONTAL_CROSSES_AUTOZERO,
'horizontal_crosses_value' => null,
'textRotation' => null,
'hidden' => null,
'majorTimeUnit' => self::TIME_UNIT_YEARS,
'minorTimeUnit' => self::TIME_UNIT_MONTHS,
'baseTimeUnit' => self::TIME_UNIT_DAYS,
'logBase' => null,
'dispUnitsBuiltIn' => null,
];
public const DISP_UNITS_HUNDREDS = 'hundreds';
public const DISP_UNITS_THOUSANDS = 'thousands';
public const DISP_UNITS_TEN_THOUSANDS = 'tenThousands';
public const DISP_UNITS_HUNDRED_THOUSANDS = 'hundredThousands';
public const DISP_UNITS_MILLIONS = 'millions';
public const DISP_UNITS_TEN_MILLIONS = 'tenMillions';
public const DISP_UNITS_HUNDRED_MILLIONS = 'hundredMillions';
public const DISP_UNITS_BILLIONS = 'billions';
public const DISP_UNITS_TRILLIONS = 'trillions';
public const DISP_UNITS_BUILTIN_INT = [
100 => self::DISP_UNITS_HUNDREDS,
1000 => self::DISP_UNITS_THOUSANDS,
10000 => self::DISP_UNITS_TEN_THOUSANDS,
100000 => self::DISP_UNITS_HUNDRED_THOUSANDS,
1000000 => self::DISP_UNITS_MILLIONS,
10000000 => self::DISP_UNITS_TEN_MILLIONS,
100000000 => self::DISP_UNITS_HUNDRED_MILLIONS,
1000000000 => self::DISP_UNITS_BILLIONS,
1000000000000 => self::DISP_UNITS_TRILLIONS,
];
/**
* Fill Properties.
*/
private ChartColor $fillColor;
private const NUMERIC_FORMAT = [
Properties::FORMAT_CODE_NUMBER,
Properties::FORMAT_CODE_DATE,
Properties::FORMAT_CODE_DATE_ISO8601,
];
private bool $noFill = false;
/**
* Get Series Data Type.
*/
public function setAxisNumberProperties(mixed $format_code, ?bool $numeric = null, int $sourceLinked = 0): void
{
$format = (string) $format_code;
$this->axisNumber['format'] = $format;
$this->axisNumber['source_linked'] = $sourceLinked;
if (is_bool($numeric)) {
$this->axisNumber['numeric'] = $numeric;
} elseif (in_array($format, self::NUMERIC_FORMAT, true)) {
$this->axisNumber['numeric'] = true;
}
}
/**
* Get Axis Number Format Data Type.
*/
public function getAxisNumberFormat(): string
{
return $this->axisNumber['format'];
}
/**
* Get Axis Number Source Linked.
*/
public function getAxisNumberSourceLinked(): string
{
return (string) $this->axisNumber['source_linked'];
}
public function getAxisIsNumericFormat(): bool
{
return $this->axisType === self::AXIS_TYPE_DATE || (bool) $this->axisNumber['numeric'];
}
public function setAxisOption(string $key, null|float|int|string $value): void
{
if ($value !== null && $value !== '') {
$this->axisOptions[$key] = (string) $value;
}
}
/**
* Set Axis Options Properties.
*/
public function setAxisOptionsProperties(
string $axisLabels,
?string $horizontalCrossesValue = null,
?string $horizontalCrosses = null,
?string $axisOrientation = null,
?string $majorTmt = null,
?string $minorTmt = null,
null|float|int|string $minimum = null,
null|float|int|string $maximum = null,
null|float|int|string $majorUnit = null,
null|float|int|string $minorUnit = null,
null|float|int|string $textRotation = null,
?string $hidden = null,
?string $baseTimeUnit = null,
?string $majorTimeUnit = null,
?string $minorTimeUnit = null,
null|float|int|string $logBase = null,
?string $dispUnitsBuiltIn = null
): void {
$this->axisOptions['axis_labels'] = $axisLabels;
$this->setAxisOption('horizontal_crosses_value', $horizontalCrossesValue);
$this->setAxisOption('horizontal_crosses', $horizontalCrosses);
$this->setAxisOption('orientation', $axisOrientation);
$this->setAxisOption('major_tick_mark', $majorTmt);
$this->setAxisOption('minor_tick_mark', $minorTmt);
$this->setAxisOption('minimum', $minimum);
$this->setAxisOption('maximum', $maximum);
$this->setAxisOption('major_unit', $majorUnit);
$this->setAxisOption('minor_unit', $minorUnit);
$this->setAxisOption('textRotation', $textRotation);
$this->setAxisOption('hidden', $hidden);
$this->setAxisOption('baseTimeUnit', $baseTimeUnit);
$this->setAxisOption('majorTimeUnit', $majorTimeUnit);
$this->setAxisOption('minorTimeUnit', $minorTimeUnit);
$this->setAxisOption('logBase', $logBase);
$this->setAxisOption('dispUnitsBuiltIn', $dispUnitsBuiltIn);
}
/**
* Get Axis Options Property.
*/
public function getAxisOptionsProperty(string $property): ?string
{
if ($property === 'textRotation') {
if ($this->axisText !== null) {
if ($this->axisText->getRotation() !== null) {
return (string) $this->axisText->getRotation();
}
}
}
return $this->axisOptions[$property];
}
/**
* Set Axis Orientation Property.
*/
public function setAxisOrientation(string $orientation): void
{
$this->axisOptions['orientation'] = (string) $orientation;
}
public function getAxisType(): string
{
return $this->axisType;
}
public function setAxisType(string $type): self
{
if ($type === self::AXIS_TYPE_CATEGORY || $type === self::AXIS_TYPE_VALUE || $type === self::AXIS_TYPE_DATE) {
$this->axisType = $type;
} else {
$this->axisType = '';
}
return $this;
}
/**
* Set Fill Property.
*/
public function setFillParameters(?string $color, ?int $alpha = null, ?string $AlphaType = ChartColor::EXCEL_COLOR_TYPE_RGB): void
{
$this->fillColor->setColorProperties($color, $alpha, $AlphaType);
}
/**
* Get Fill Property.
*/
public function getFillProperty(string $property): string
{
return (string) $this->fillColor->getColorProperty($property);
}
public function getFillColorObject(): ChartColor
{
return $this->fillColor;
}
private string $crossBetween = ''; // 'between' or 'midCat' might be better
public function setCrossBetween(string $crossBetween): self
{
$this->crossBetween = $crossBetween;
return $this;
}
public function getCrossBetween(): string
{
return $this->crossBetween;
}
public function getMajorGridlines(): ?GridLines
{
return $this->majorGridlines;
}
public function getMinorGridlines(): ?GridLines
{
return $this->minorGridlines;
}
public function setMajorGridlines(?GridLines $gridlines): self
{
$this->majorGridlines = $gridlines;
return $this;
}
public function setMinorGridlines(?GridLines $gridlines): self
{
$this->minorGridlines = $gridlines;
return $this;
}
public function getAxisText(): ?AxisText
{
return $this->axisText;
}
public function setAxisText(?AxisText $axisText): self
{
$this->axisText = $axisText;
return $this;
}
public function setNoFill(bool $noFill): self
{
$this->noFill = $noFill;
return $this;
}
public function getNoFill(): bool
{
return $this->noFill;
}
public function setDispUnitsTitle(?Title $dispUnitsTitle): self
{
$this->dispUnitsTitle = $dispUnitsTitle;
return $this;
}
public function getDispUnitsTitle(): ?Title
{
return $this->dispUnitsTitle;
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
parent::__clone();
$this->majorGridlines = ($this->majorGridlines === null) ? null : clone $this->majorGridlines;
$this->majorGridlines = ($this->minorGridlines === null) ? null : clone $this->minorGridlines;
$this->axisText = ($this->axisText === null) ? null : clone $this->axisText;
$this->dispUnitsTitle = ($this->dispUnitsTitle === null) ? null : clone $this->dispUnitsTitle;
$this->fillColor = clone $this->fillColor;
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart;
use PhpOffice\PhpSpreadsheet\Style\Font;
class AxisText extends Properties
{
private ?int $rotation = null;
private Font $font;
public function __construct()
{
parent::__construct();
$this->font = new Font();
$this->font->setSize(null, true);
}
public function setRotation(?int $rotation): self
{
$this->rotation = $rotation;
return $this;
}
public function getRotation(): ?int
{
return $this->rotation;
}
public function getFillColorObject(): ChartColor
{
$fillColor = $this->font->getChartColor();
if ($fillColor === null) {
$fillColor = new ChartColor();
$this->font->setChartColorFromObject($fillColor);
}
return $fillColor;
}
public function getFont(): Font
{
return $this->font;
}
public function setFont(Font $font): self
{
$this->font = $font;
return $this;
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
parent::__clone();
$this->font = clone $this->font;
}
}

View file

@ -0,0 +1,770 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart;
use PhpOffice\PhpSpreadsheet\Settings;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class Chart
{
/**
* Chart Name.
*/
private string $name;
/**
* Worksheet.
*/
private ?Worksheet $worksheet = null;
/**
* Chart Title.
*/
private ?Title $title;
/**
* Chart Legend.
*/
private ?Legend $legend;
/**
* X-Axis Label.
*/
private ?Title $xAxisLabel;
/**
* Y-Axis Label.
*/
private ?Title $yAxisLabel;
/**
* Chart Plot Area.
*/
private ?PlotArea $plotArea;
/**
* Plot Visible Only.
*/
private bool $plotVisibleOnly;
/**
* Display Blanks as.
*/
private string $displayBlanksAs;
/**
* Chart Asix Y as.
*/
private Axis $yAxis;
/**
* Chart Asix X as.
*/
private Axis $xAxis;
/**
* Top-Left Cell Position.
*/
private string $topLeftCellRef = 'A1';
/**
* Top-Left X-Offset.
*/
private int $topLeftXOffset = 0;
/**
* Top-Left Y-Offset.
*/
private int $topLeftYOffset = 0;
/**
* Bottom-Right Cell Position.
*/
private string $bottomRightCellRef = '';
/**
* Bottom-Right X-Offset.
*/
private int $bottomRightXOffset = 10;
/**
* Bottom-Right Y-Offset.
*/
private int $bottomRightYOffset = 10;
private ?int $rotX = null;
private ?int $rotY = null;
private ?int $rAngAx = null;
private ?int $perspective = null;
private bool $oneCellAnchor = false;
private bool $autoTitleDeleted = false;
private bool $noFill = false;
private bool $roundedCorners = false;
private GridLines $borderLines;
private ChartColor $fillColor;
/**
* Rendered width in pixels.
*/
private ?float $renderedWidth = null;
/**
* Rendered height in pixels.
*/
private ?float $renderedHeight = null;
/**
* Create a new Chart.
* majorGridlines and minorGridlines are deprecated, moved to Axis.
*/
public function __construct(mixed $name, ?Title $title = null, ?Legend $legend = null, ?PlotArea $plotArea = null, mixed $plotVisibleOnly = true, string $displayBlanksAs = DataSeries::EMPTY_AS_GAP, ?Title $xAxisLabel = null, ?Title $yAxisLabel = null, ?Axis $xAxis = null, ?Axis $yAxis = null, ?GridLines $majorGridlines = null, ?GridLines $minorGridlines = null)
{
$this->name = $name;
$this->title = $title;
$this->legend = $legend;
$this->xAxisLabel = $xAxisLabel;
$this->yAxisLabel = $yAxisLabel;
$this->plotArea = $plotArea;
$this->plotVisibleOnly = $plotVisibleOnly;
$this->displayBlanksAs = $displayBlanksAs;
$this->xAxis = $xAxis ?? new Axis();
$this->yAxis = $yAxis ?? new Axis();
if ($majorGridlines !== null) {
$this->yAxis->setMajorGridlines($majorGridlines);
}
if ($minorGridlines !== null) {
$this->yAxis->setMinorGridlines($minorGridlines);
}
$this->fillColor = new ChartColor();
$this->borderLines = new GridLines();
}
public function __destruct()
{
$this->worksheet = null;
}
/**
* Get Name.
*/
public function getName(): string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
/**
* Get Worksheet.
*/
public function getWorksheet(): ?Worksheet
{
return $this->worksheet;
}
/**
* Set Worksheet.
*
* @return $this
*/
public function setWorksheet(?Worksheet $worksheet = null): static
{
$this->worksheet = $worksheet;
return $this;
}
public function getTitle(): ?Title
{
return $this->title;
}
/**
* Set Title.
*
* @return $this
*/
public function setTitle(Title $title): static
{
$this->title = $title;
return $this;
}
public function getLegend(): ?Legend
{
return $this->legend;
}
/**
* Set Legend.
*
* @return $this
*/
public function setLegend(Legend $legend): static
{
$this->legend = $legend;
return $this;
}
public function getXAxisLabel(): ?Title
{
return $this->xAxisLabel;
}
/**
* Set X-Axis Label.
*
* @return $this
*/
public function setXAxisLabel(Title $label): static
{
$this->xAxisLabel = $label;
return $this;
}
public function getYAxisLabel(): ?Title
{
return $this->yAxisLabel;
}
/**
* Set Y-Axis Label.
*
* @return $this
*/
public function setYAxisLabel(Title $label): static
{
$this->yAxisLabel = $label;
return $this;
}
public function getPlotArea(): ?PlotArea
{
return $this->plotArea;
}
public function getPlotAreaOrThrow(): PlotArea
{
$plotArea = $this->getPlotArea();
if ($plotArea !== null) {
return $plotArea;
}
throw new Exception('Chart has no PlotArea');
}
/**
* Set Plot Area.
*/
public function setPlotArea(PlotArea $plotArea): self
{
$this->plotArea = $plotArea;
return $this;
}
/**
* Get Plot Visible Only.
*/
public function getPlotVisibleOnly(): bool
{
return $this->plotVisibleOnly;
}
/**
* Set Plot Visible Only.
*
* @return $this
*/
public function setPlotVisibleOnly(bool $plotVisibleOnly): static
{
$this->plotVisibleOnly = $plotVisibleOnly;
return $this;
}
/**
* Get Display Blanks as.
*/
public function getDisplayBlanksAs(): string
{
return $this->displayBlanksAs;
}
/**
* Set Display Blanks as.
*
* @return $this
*/
public function setDisplayBlanksAs(string $displayBlanksAs): static
{
$this->displayBlanksAs = $displayBlanksAs;
return $this;
}
public function getChartAxisY(): Axis
{
return $this->yAxis;
}
/**
* Set yAxis.
*/
public function setChartAxisY(?Axis $axis): self
{
$this->yAxis = $axis ?? new Axis();
return $this;
}
public function getChartAxisX(): Axis
{
return $this->xAxis;
}
/**
* Set xAxis.
*/
public function setChartAxisX(?Axis $axis): self
{
$this->xAxis = $axis ?? new Axis();
return $this;
}
/**
* Set the Top Left position for the chart.
*
* @return $this
*/
public function setTopLeftPosition(string $cellAddress, ?int $xOffset = null, ?int $yOffset = null): static
{
$this->topLeftCellRef = $cellAddress;
if ($xOffset !== null) {
$this->setTopLeftXOffset($xOffset);
}
if ($yOffset !== null) {
$this->setTopLeftYOffset($yOffset);
}
return $this;
}
/**
* Get the top left position of the chart.
*
* Returns ['cell' => string cell address, 'xOffset' => int, 'yOffset' => int].
*
* @return array{cell: string, xOffset: int, yOffset: int} an associative array containing the cell address, X-Offset and Y-Offset from the top left of that cell
*/
public function getTopLeftPosition(): array
{
return [
'cell' => $this->topLeftCellRef,
'xOffset' => $this->topLeftXOffset,
'yOffset' => $this->topLeftYOffset,
];
}
/**
* Get the cell address where the top left of the chart is fixed.
*/
public function getTopLeftCell(): string
{
return $this->topLeftCellRef;
}
/**
* Set the Top Left cell position for the chart.
*
* @return $this
*/
public function setTopLeftCell(string $cellAddress): static
{
$this->topLeftCellRef = $cellAddress;
return $this;
}
/**
* Set the offset position within the Top Left cell for the chart.
*
* @return $this
*/
public function setTopLeftOffset(?int $xOffset, ?int $yOffset): static
{
if ($xOffset !== null) {
$this->setTopLeftXOffset($xOffset);
}
if ($yOffset !== null) {
$this->setTopLeftYOffset($yOffset);
}
return $this;
}
/**
* Get the offset position within the Top Left cell for the chart.
*
* @return int[]
*/
public function getTopLeftOffset(): array
{
return [
'X' => $this->topLeftXOffset,
'Y' => $this->topLeftYOffset,
];
}
/**
* @return $this
*/
public function setTopLeftXOffset(int $xOffset): static
{
$this->topLeftXOffset = $xOffset;
return $this;
}
public function getTopLeftXOffset(): int
{
return $this->topLeftXOffset;
}
/**
* @return $this
*/
public function setTopLeftYOffset(int $yOffset): static
{
$this->topLeftYOffset = $yOffset;
return $this;
}
public function getTopLeftYOffset(): int
{
return $this->topLeftYOffset;
}
/**
* Set the Bottom Right position of the chart.
*
* @return $this
*/
public function setBottomRightPosition(string $cellAddress = '', ?int $xOffset = null, ?int $yOffset = null): static
{
$this->bottomRightCellRef = $cellAddress;
if ($xOffset !== null) {
$this->setBottomRightXOffset($xOffset);
}
if ($yOffset !== null) {
$this->setBottomRightYOffset($yOffset);
}
return $this;
}
/**
* Get the bottom right position of the chart.
*
* @return array an associative array containing the cell address, X-Offset and Y-Offset from the top left of that cell
*/
public function getBottomRightPosition(): array
{
return [
'cell' => $this->bottomRightCellRef,
'xOffset' => $this->bottomRightXOffset,
'yOffset' => $this->bottomRightYOffset,
];
}
/**
* Set the Bottom Right cell for the chart.
*
* @return $this
*/
public function setBottomRightCell(string $cellAddress = ''): static
{
$this->bottomRightCellRef = $cellAddress;
return $this;
}
/**
* Get the cell address where the bottom right of the chart is fixed.
*/
public function getBottomRightCell(): string
{
return $this->bottomRightCellRef;
}
/**
* Set the offset position within the Bottom Right cell for the chart.
*
* @return $this
*/
public function setBottomRightOffset(?int $xOffset, ?int $yOffset): static
{
if ($xOffset !== null) {
$this->setBottomRightXOffset($xOffset);
}
if ($yOffset !== null) {
$this->setBottomRightYOffset($yOffset);
}
return $this;
}
/**
* Get the offset position within the Bottom Right cell for the chart.
*
* @return int[]
*/
public function getBottomRightOffset(): array
{
return [
'X' => $this->bottomRightXOffset,
'Y' => $this->bottomRightYOffset,
];
}
/**
* @return $this
*/
public function setBottomRightXOffset(int $xOffset): static
{
$this->bottomRightXOffset = $xOffset;
return $this;
}
public function getBottomRightXOffset(): int
{
return $this->bottomRightXOffset;
}
/**
* @return $this
*/
public function setBottomRightYOffset(int $yOffset): static
{
$this->bottomRightYOffset = $yOffset;
return $this;
}
public function getBottomRightYOffset(): int
{
return $this->bottomRightYOffset;
}
public function refresh(): void
{
if ($this->worksheet !== null && $this->plotArea !== null) {
$this->plotArea->refresh($this->worksheet);
}
}
/**
* Render the chart to given file (or stream).
*
* @param ?string $outputDestination Name of the file render to
*
* @return bool true on success
*/
public function render(?string $outputDestination = null): bool
{
if ($outputDestination == 'php://output') {
$outputDestination = null;
}
$libraryName = Settings::getChartRenderer();
if ($libraryName === null) {
return false;
}
// Ensure that data series values are up-to-date before we render
$this->refresh();
$renderer = new $libraryName($this);
return $renderer->render($outputDestination);
}
public function getRotX(): ?int
{
return $this->rotX;
}
public function setRotX(?int $rotX): self
{
$this->rotX = $rotX;
return $this;
}
public function getRotY(): ?int
{
return $this->rotY;
}
public function setRotY(?int $rotY): self
{
$this->rotY = $rotY;
return $this;
}
public function getRAngAx(): ?int
{
return $this->rAngAx;
}
public function setRAngAx(?int $rAngAx): self
{
$this->rAngAx = $rAngAx;
return $this;
}
public function getPerspective(): ?int
{
return $this->perspective;
}
public function setPerspective(?int $perspective): self
{
$this->perspective = $perspective;
return $this;
}
public function getOneCellAnchor(): bool
{
return $this->oneCellAnchor;
}
public function setOneCellAnchor(bool $oneCellAnchor): self
{
$this->oneCellAnchor = $oneCellAnchor;
return $this;
}
public function getAutoTitleDeleted(): bool
{
return $this->autoTitleDeleted;
}
public function setAutoTitleDeleted(bool $autoTitleDeleted): self
{
$this->autoTitleDeleted = $autoTitleDeleted;
return $this;
}
public function getNoFill(): bool
{
return $this->noFill;
}
public function setNoFill(bool $noFill): self
{
$this->noFill = $noFill;
return $this;
}
public function getRoundedCorners(): bool
{
return $this->roundedCorners;
}
public function setRoundedCorners(?bool $roundedCorners): self
{
if ($roundedCorners !== null) {
$this->roundedCorners = $roundedCorners;
}
return $this;
}
public function getBorderLines(): GridLines
{
return $this->borderLines;
}
public function setBorderLines(GridLines $borderLines): self
{
$this->borderLines = $borderLines;
return $this;
}
public function getFillColor(): ChartColor
{
return $this->fillColor;
}
public function setRenderedWidth(?float $width): self
{
$this->renderedWidth = $width;
return $this;
}
public function getRenderedWidth(): ?float
{
return $this->renderedWidth;
}
public function setRenderedHeight(?float $height): self
{
$this->renderedHeight = $height;
return $this;
}
public function getRenderedHeight(): ?float
{
return $this->renderedHeight;
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$this->worksheet = null;
$this->title = ($this->title === null) ? null : clone $this->title;
$this->legend = ($this->legend === null) ? null : clone $this->legend;
$this->xAxisLabel = ($this->xAxisLabel === null) ? null : clone $this->xAxisLabel;
$this->yAxisLabel = ($this->yAxisLabel === null) ? null : clone $this->yAxisLabel;
$this->plotArea = ($this->plotArea === null) ? null : clone $this->plotArea;
$this->xAxis = clone $this->xAxis;
$this->yAxis = clone $this->yAxis;
$this->borderLines = clone $this->borderLines;
$this->fillColor = clone $this->fillColor;
}
}

View file

@ -0,0 +1,160 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart;
class ChartColor
{
const EXCEL_COLOR_TYPE_STANDARD = 'prstClr';
const EXCEL_COLOR_TYPE_SCHEME = 'schemeClr';
const EXCEL_COLOR_TYPE_RGB = 'srgbClr';
const EXCEL_COLOR_TYPES = [
self::EXCEL_COLOR_TYPE_RGB,
self::EXCEL_COLOR_TYPE_SCHEME,
self::EXCEL_COLOR_TYPE_STANDARD,
];
private string $value = '';
private string $type = '';
private ?int $alpha = null;
private ?int $brightness = null;
/**
* @param string|string[] $value
*/
public function __construct($value = '', ?int $alpha = null, ?string $type = null, ?int $brightness = null)
{
if (is_array($value)) {
$this->setColorPropertiesArray($value);
} else {
$this->setColorProperties($value, $alpha, $type, $brightness);
}
}
public function getValue(): string
{
return $this->value;
}
public function setValue(string $value): self
{
$this->value = $value;
return $this;
}
public function getType(): string
{
return $this->type;
}
public function setType(string $type): self
{
$this->type = $type;
return $this;
}
public function getAlpha(): ?int
{
return $this->alpha;
}
public function setAlpha(?int $alpha): self
{
$this->alpha = $alpha;
return $this;
}
public function getBrightness(): ?int
{
return $this->brightness;
}
public function setBrightness(?int $brightness): self
{
$this->brightness = $brightness;
return $this;
}
public function setColorProperties(?string $color, null|float|int|string $alpha = null, ?string $type = null, null|float|int|string $brightness = null): self
{
if (empty($type) && !empty($color)) {
if (str_starts_with($color, '*')) {
$type = 'schemeClr';
$color = substr($color, 1);
} elseif (str_starts_with($color, '/')) {
$type = 'prstClr';
$color = substr($color, 1);
} elseif (preg_match('/^[0-9A-Fa-f]{6}$/', $color) === 1) {
$type = 'srgbClr';
}
}
if ($color !== null) {
$this->setValue("$color");
}
if ($type !== null) {
$this->setType($type);
}
if ($alpha === null) {
$this->setAlpha(null);
} elseif (is_numeric($alpha)) {
$this->setAlpha((int) $alpha);
}
if ($brightness === null) {
$this->setBrightness(null);
} elseif (is_numeric($brightness)) {
$this->setBrightness((int) $brightness);
}
return $this;
}
public function setColorPropertiesArray(array $color): self
{
return $this->setColorProperties(
$color['value'] ?? '',
$color['alpha'] ?? null,
$color['type'] ?? null,
$color['brightness'] ?? null
);
}
public function isUsable(): bool
{
return $this->type !== '' && $this->value !== '';
}
/**
* Get Color Property.
*/
public function getColorProperty(string $propertyName): null|int|string
{
$retVal = null;
if ($propertyName === 'value') {
$retVal = $this->value;
} elseif ($propertyName === 'type') {
$retVal = $this->type;
} elseif ($propertyName === 'alpha') {
$retVal = $this->alpha;
} elseif ($propertyName === 'brightness') {
$retVal = $this->brightness;
}
return $retVal;
}
public static function alphaToXml(int $alpha): string
{
return (string) (100 - $alpha) . '000';
}
public static function alphaFromXml(float|int|string $alpha): int
{
return 100 - ((int) $alpha / 1000);
}
}

View file

@ -0,0 +1,412 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class DataSeries
{
const TYPE_BARCHART = 'barChart';
const TYPE_BARCHART_3D = 'bar3DChart';
const TYPE_LINECHART = 'lineChart';
const TYPE_LINECHART_3D = 'line3DChart';
const TYPE_AREACHART = 'areaChart';
const TYPE_AREACHART_3D = 'area3DChart';
const TYPE_PIECHART = 'pieChart';
const TYPE_PIECHART_3D = 'pie3DChart';
const TYPE_DOUGHNUTCHART = 'doughnutChart';
const TYPE_DONUTCHART = self::TYPE_DOUGHNUTCHART; // Synonym
const TYPE_SCATTERCHART = 'scatterChart';
const TYPE_SURFACECHART = 'surfaceChart';
const TYPE_SURFACECHART_3D = 'surface3DChart';
const TYPE_RADARCHART = 'radarChart';
const TYPE_BUBBLECHART = 'bubbleChart';
const TYPE_STOCKCHART = 'stockChart';
const TYPE_CANDLECHART = self::TYPE_STOCKCHART; // Synonym
const GROUPING_CLUSTERED = 'clustered';
const GROUPING_STACKED = 'stacked';
const GROUPING_PERCENT_STACKED = 'percentStacked';
const GROUPING_STANDARD = 'standard';
const DIRECTION_BAR = 'bar';
const DIRECTION_HORIZONTAL = self::DIRECTION_BAR;
const DIRECTION_COL = 'col';
const DIRECTION_COLUMN = self::DIRECTION_COL;
const DIRECTION_VERTICAL = self::DIRECTION_COL;
const STYLE_LINEMARKER = 'lineMarker';
const STYLE_SMOOTHMARKER = 'smoothMarker';
const STYLE_MARKER = 'marker';
const STYLE_FILLED = 'filled';
const EMPTY_AS_GAP = 'gap';
const EMPTY_AS_ZERO = 'zero';
const EMPTY_AS_SPAN = 'span';
/**
* Series Plot Type.
*/
private ?string $plotType;
/**
* Plot Grouping Type.
*/
private ?string $plotGrouping;
/**
* Plot Direction.
*/
private string $plotDirection;
/**
* Plot Style.
*/
private ?string $plotStyle;
/**
* Order of plots in Series.
*
* @var int[]
*/
private array $plotOrder;
/**
* Plot Label.
*
* @var DataSeriesValues[]
*/
private array $plotLabel;
/**
* Plot Category.
*
* @var DataSeriesValues[]
*/
private array $plotCategory;
/**
* Smooth Line. Must be specified for both DataSeries and DataSeriesValues.
*/
private bool $smoothLine;
/**
* Plot Values.
*
* @var DataSeriesValues[]
*/
private array $plotValues;
/**
* Plot Bubble Sizes.
*
* @var DataSeriesValues[]
*/
private array $plotBubbleSizes = [];
/**
* Create a new DataSeries.
*
* @param int[] $plotOrder
* @param DataSeriesValues[] $plotLabel
* @param DataSeriesValues[] $plotCategory
* @param DataSeriesValues[] $plotValues
*/
public function __construct(
null|string $plotType = null,
null|string $plotGrouping = null,
array $plotOrder = [],
array $plotLabel = [],
array $plotCategory = [],
array $plotValues = [],
?string $plotDirection = null,
bool $smoothLine = false,
?string $plotStyle = null
) {
$this->plotType = $plotType;
$this->plotGrouping = $plotGrouping;
$this->plotOrder = $plotOrder;
$keys = array_keys($plotValues);
$this->plotValues = $plotValues;
if (!isset($plotLabel[$keys[0]])) {
$plotLabel[$keys[0]] = new DataSeriesValues();
}
$this->plotLabel = $plotLabel;
if (!isset($plotCategory[$keys[0]])) {
$plotCategory[$keys[0]] = new DataSeriesValues();
}
$this->plotCategory = $plotCategory;
$this->smoothLine = (bool) $smoothLine;
$this->plotStyle = $plotStyle;
if ($plotDirection === null) {
$plotDirection = self::DIRECTION_COL;
}
$this->plotDirection = $plotDirection;
}
/**
* Get Plot Type.
*/
public function getPlotType(): ?string
{
return $this->plotType;
}
/**
* Set Plot Type.
*
* @return $this
*/
public function setPlotType(string $plotType): static
{
$this->plotType = $plotType;
return $this;
}
/**
* Get Plot Grouping Type.
*/
public function getPlotGrouping(): ?string
{
return $this->plotGrouping;
}
/**
* Set Plot Grouping Type.
*
* @return $this
*/
public function setPlotGrouping(string $groupingType): static
{
$this->plotGrouping = $groupingType;
return $this;
}
/**
* Get Plot Direction.
*/
public function getPlotDirection(): string
{
return $this->plotDirection;
}
/**
* Set Plot Direction.
*
* @return $this
*/
public function setPlotDirection(string $plotDirection): static
{
$this->plotDirection = $plotDirection;
return $this;
}
/**
* Get Plot Order.
*
* @return int[]
*/
public function getPlotOrder(): array
{
return $this->plotOrder;
}
/**
* Get Plot Labels.
*
* @return DataSeriesValues[]
*/
public function getPlotLabels(): array
{
return $this->plotLabel;
}
/**
* Get Plot Label by Index.
*
* @return DataSeriesValues|false
*/
public function getPlotLabelByIndex(mixed $index): bool|DataSeriesValues
{
$keys = array_keys($this->plotLabel);
if (in_array($index, $keys)) {
return $this->plotLabel[$index];
}
return false;
}
/**
* Get Plot Categories.
*
* @return DataSeriesValues[]
*/
public function getPlotCategories(): array
{
return $this->plotCategory;
}
/**
* Get Plot Category by Index.
*
* @return DataSeriesValues|false
*/
public function getPlotCategoryByIndex(mixed $index): bool|DataSeriesValues
{
$keys = array_keys($this->plotCategory);
if (in_array($index, $keys)) {
return $this->plotCategory[$index];
} elseif (isset($keys[$index])) {
return $this->plotCategory[$keys[$index]];
}
return false;
}
/**
* Get Plot Style.
*/
public function getPlotStyle(): ?string
{
return $this->plotStyle;
}
/**
* Set Plot Style.
*
* @return $this
*/
public function setPlotStyle(?string $plotStyle): static
{
$this->plotStyle = $plotStyle;
return $this;
}
/**
* Get Plot Values.
*
* @return DataSeriesValues[]
*/
public function getPlotValues(): array
{
return $this->plotValues;
}
/**
* Get Plot Values by Index.
*
* @return DataSeriesValues|false
*/
public function getPlotValuesByIndex(mixed $index): bool|DataSeriesValues
{
$keys = array_keys($this->plotValues);
if (in_array($index, $keys)) {
return $this->plotValues[$index];
}
return false;
}
/**
* Get Plot Bubble Sizes.
*
* @return DataSeriesValues[]
*/
public function getPlotBubbleSizes(): array
{
return $this->plotBubbleSizes;
}
/**
* Set Plot Bubble Sizes.
*
* @param DataSeriesValues[] $plotBubbleSizes
*/
public function setPlotBubbleSizes(array $plotBubbleSizes): self
{
$this->plotBubbleSizes = $plotBubbleSizes;
return $this;
}
/**
* Get Number of Plot Series.
*/
public function getPlotSeriesCount(): int
{
return count($this->plotValues);
}
/**
* Get Smooth Line.
*/
public function getSmoothLine(): bool
{
return $this->smoothLine;
}
/**
* Set Smooth Line.
*
* @return $this
*/
public function setSmoothLine(bool $smoothLine): static
{
$this->smoothLine = $smoothLine;
return $this;
}
public function refresh(Worksheet $worksheet): void
{
foreach ($this->plotValues as $plotValues) {
if ($plotValues !== null) {
$plotValues->refresh($worksheet, true);
}
}
foreach ($this->plotLabel as $plotValues) {
if ($plotValues !== null) {
$plotValues->refresh($worksheet, true);
}
}
foreach ($this->plotCategory as $plotValues) {
if ($plotValues !== null) {
$plotValues->refresh($worksheet, false);
}
}
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$plotLabels = $this->plotLabel;
$this->plotLabel = [];
foreach ($plotLabels as $plotLabel) {
$this->plotLabel[] = $plotLabel;
}
$plotCategories = $this->plotCategory;
$this->plotCategory = [];
foreach ($plotCategories as $plotCategory) {
$this->plotCategory[] = clone $plotCategory;
}
$plotValues = $this->plotValues;
$this->plotValues = [];
foreach ($plotValues as $plotValue) {
$this->plotValues[] = clone $plotValue;
}
$plotBubbleSizes = $this->plotBubbleSizes;
$this->plotBubbleSizes = [];
foreach ($plotBubbleSizes as $plotBubbleSize) {
$this->plotBubbleSizes[] = clone $plotBubbleSize;
}
}
}

View file

@ -0,0 +1,566 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class DataSeriesValues extends Properties
{
const DATASERIES_TYPE_STRING = 'String';
const DATASERIES_TYPE_NUMBER = 'Number';
private const DATA_TYPE_VALUES = [
self::DATASERIES_TYPE_STRING,
self::DATASERIES_TYPE_NUMBER,
];
/**
* Series Data Type.
*/
private string $dataType;
/**
* Series Data Source.
*/
private ?string $dataSource;
/**
* Format Code.
*/
private ?string $formatCode;
/**
* Series Point Marker.
*/
private ?string $pointMarker;
private ChartColor $markerFillColor;
private ChartColor $markerBorderColor;
/**
* Series Point Size.
*/
private int $pointSize = 3;
/**
* Point Count (The number of datapoints in the dataseries).
*/
private int $pointCount;
/**
* Data Values.
*/
private ?array $dataValues;
/**
* Fill color (can be array with colors if dataseries have custom colors).
*
* @var null|ChartColor|ChartColor[]
*/
private $fillColor;
private bool $scatterLines = true;
private bool $bubble3D = false;
private ?Layout $labelLayout = null;
/** @var TrendLine[] */
private array $trendLines = [];
/**
* Create a new DataSeriesValues object.
*
* @param null|ChartColor|ChartColor[]|string|string[] $fillColor
*/
public function __construct(
string $dataType = self::DATASERIES_TYPE_NUMBER,
?string $dataSource = null,
?string $formatCode = null,
int $pointCount = 0,
?array $dataValues = [],
?string $marker = null,
null|ChartColor|array|string $fillColor = null,
int|string $pointSize = 3
) {
parent::__construct();
$this->markerFillColor = new ChartColor();
$this->markerBorderColor = new ChartColor();
$this->setDataType($dataType);
$this->dataSource = $dataSource;
$this->formatCode = $formatCode;
$this->pointCount = $pointCount;
$this->dataValues = $dataValues;
$this->pointMarker = $marker;
if ($fillColor !== null) {
$this->setFillColor($fillColor);
}
if (is_numeric($pointSize)) {
$this->pointSize = (int) $pointSize;
}
}
/**
* Get Series Data Type.
*/
public function getDataType(): string
{
return $this->dataType;
}
/**
* Set Series Data Type.
*
* @param string $dataType Datatype of this data series
* Typical values are:
* DataSeriesValues::DATASERIES_TYPE_STRING
* Normally used for axis point values
* DataSeriesValues::DATASERIES_TYPE_NUMBER
* Normally used for chart data values
*
* @return $this
*/
public function setDataType(string $dataType): static
{
if (!in_array($dataType, self::DATA_TYPE_VALUES)) {
throw new Exception('Invalid datatype for chart data series values');
}
$this->dataType = $dataType;
return $this;
}
/**
* Get Series Data Source (formula).
*/
public function getDataSource(): ?string
{
return $this->dataSource;
}
/**
* Set Series Data Source (formula).
*
* @return $this
*/
public function setDataSource(?string $dataSource): static
{
$this->dataSource = $dataSource;
return $this;
}
/**
* Get Point Marker.
*/
public function getPointMarker(): ?string
{
return $this->pointMarker;
}
/**
* Set Point Marker.
*
* @return $this
*/
public function setPointMarker(string $marker): static
{
$this->pointMarker = $marker;
return $this;
}
public function getMarkerFillColor(): ChartColor
{
return $this->markerFillColor;
}
public function getMarkerBorderColor(): ChartColor
{
return $this->markerBorderColor;
}
/**
* Get Point Size.
*/
public function getPointSize(): int
{
return $this->pointSize;
}
/**
* Set Point Size.
*
* @return $this
*/
public function setPointSize(int $size = 3): static
{
$this->pointSize = $size;
return $this;
}
/**
* Get Series Format Code.
*/
public function getFormatCode(): ?string
{
return $this->formatCode;
}
/**
* Set Series Format Code.
*
* @return $this
*/
public function setFormatCode(string $formatCode): static
{
$this->formatCode = $formatCode;
return $this;
}
/**
* Get Series Point Count.
*/
public function getPointCount(): int
{
return $this->pointCount;
}
/**
* Get fill color object.
*
* @return null|ChartColor|ChartColor[]
*/
public function getFillColorObject()
{
return $this->fillColor;
}
private function stringToChartColor(string $fillString): ChartColor
{
$value = $type = '';
if (str_starts_with($fillString, '*')) {
$type = 'schemeClr';
$value = substr($fillString, 1);
} elseif (str_starts_with($fillString, '/')) {
$type = 'prstClr';
$value = substr($fillString, 1);
} elseif ($fillString !== '') {
$type = 'srgbClr';
$value = $fillString;
$this->validateColor($value);
}
return new ChartColor($value, null, $type);
}
private function chartColorToString(ChartColor $chartColor): string
{
$type = (string) $chartColor->getColorProperty('type');
$value = (string) $chartColor->getColorProperty('value');
if ($type === '' || $value === '') {
return '';
}
if ($type === 'schemeClr') {
return "*$value";
}
if ($type === 'prstClr') {
return "/$value";
}
return $value;
}
/**
* Get fill color.
*
* @return string|string[] HEX color or array with HEX colors
*/
public function getFillColor(): string|array
{
if ($this->fillColor === null) {
return '';
}
if (is_array($this->fillColor)) {
$array = [];
foreach ($this->fillColor as $chartColor) {
$array[] = $this->chartColorToString($chartColor);
}
return $array;
}
return $this->chartColorToString($this->fillColor);
}
/**
* Set fill color for series.
*
* @param ChartColor|ChartColor[]|string|string[] $color HEX color or array with HEX colors
*
* @return $this
*/
public function setFillColor($color): static
{
if (is_array($color)) {
$this->fillColor = [];
foreach ($color as $fillString) {
if ($fillString instanceof ChartColor) {
$this->fillColor[] = $fillString;
} else {
$this->fillColor[] = $this->stringToChartColor($fillString);
}
}
} elseif ($color instanceof ChartColor) {
$this->fillColor = $color;
} else {
$this->fillColor = $this->stringToChartColor($color);
}
return $this;
}
/**
* Method for validating hex color.
*
* @param string $color value for color
*
* @return bool true if validation was successful
*/
private function validateColor(string $color): bool
{
if (!preg_match('/^[a-f0-9]{6}$/i', $color)) {
throw new Exception(sprintf('Invalid hex color for chart series (color: "%s")', $color));
}
return true;
}
/**
* Get line width for series.
*/
public function getLineWidth(): null|float|int
{
return $this->lineStyleProperties['width'];
}
/**
* Set line width for the series.
*
* @return $this
*/
public function setLineWidth(null|float|int $width): static
{
$this->lineStyleProperties['width'] = $width;
return $this;
}
/**
* Identify if the Data Series is a multi-level or a simple series.
*/
public function isMultiLevelSeries(): ?bool
{
if (!empty($this->dataValues)) {
return is_array(array_values($this->dataValues)[0]);
}
return null;
}
/**
* Return the level count of a multi-level Data Series.
*/
public function multiLevelCount(): int
{
$levelCount = 0;
foreach ($this->dataValues as $dataValueSet) {
$levelCount = max($levelCount, count($dataValueSet));
}
return $levelCount;
}
/**
* Get Series Data Values.
*/
public function getDataValues(): ?array
{
return $this->dataValues;
}
/**
* Get the first Series Data value.
*/
public function getDataValue(): mixed
{
$count = count($this->dataValues);
if ($count == 0) {
return null;
} elseif ($count == 1) {
return $this->dataValues[0];
}
return $this->dataValues;
}
/**
* Set Series Data Values.
*
* @return $this
*/
public function setDataValues(array $dataValues): static
{
$this->dataValues = Functions::flattenArray($dataValues);
$this->pointCount = count($dataValues);
return $this;
}
public function refresh(Worksheet $worksheet, bool $flatten = true): void
{
if ($this->dataSource !== null) {
$calcEngine = Calculation::getInstance($worksheet->getParent());
$newDataValues = Calculation::unwrapResult(
$calcEngine->_calculateFormulaValue(
'=' . $this->dataSource,
null,
$worksheet->getCell('A1')
)
);
if ($flatten) {
$this->dataValues = Functions::flattenArray($newDataValues);
foreach ($this->dataValues as &$dataValue) {
if (is_string($dataValue) && !empty($dataValue) && $dataValue[0] == '#') {
$dataValue = 0.0;
}
}
unset($dataValue);
} else {
[$worksheet, $cellRange] = Worksheet::extractSheetTitle($this->dataSource, true);
$dimensions = Coordinate::rangeDimension(str_replace('$', '', $cellRange ?? ''));
if (($dimensions[0] == 1) || ($dimensions[1] == 1)) {
$this->dataValues = Functions::flattenArray($newDataValues);
} else {
$newArray = array_values(array_shift($newDataValues));
foreach ($newArray as $i => $newDataSet) {
$newArray[$i] = [$newDataSet];
}
foreach ($newDataValues as $newDataSet) {
$i = 0;
foreach ($newDataSet as $newDataVal) {
array_unshift($newArray[$i++], $newDataVal);
}
}
$this->dataValues = $newArray;
}
}
$this->pointCount = count($this->dataValues);
}
}
public function getScatterLines(): bool
{
return $this->scatterLines;
}
public function setScatterLines(bool $scatterLines): self
{
$this->scatterLines = $scatterLines;
return $this;
}
public function getBubble3D(): bool
{
return $this->bubble3D;
}
public function setBubble3D(bool $bubble3D): self
{
$this->bubble3D = $bubble3D;
return $this;
}
/**
* Smooth Line. Must be specified for both DataSeries and DataSeriesValues.
*/
private bool $smoothLine = false;
/**
* Get Smooth Line.
*/
public function getSmoothLine(): bool
{
return $this->smoothLine;
}
/**
* Set Smooth Line.
*
* @return $this
*/
public function setSmoothLine(bool $smoothLine): static
{
$this->smoothLine = $smoothLine;
return $this;
}
public function getLabelLayout(): ?Layout
{
return $this->labelLayout;
}
public function setLabelLayout(?Layout $labelLayout): self
{
$this->labelLayout = $labelLayout;
return $this;
}
public function setTrendLines(array $trendLines): self
{
$this->trendLines = $trendLines;
return $this;
}
public function getTrendLines(): array
{
return $this->trendLines;
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
parent::__clone();
$this->markerFillColor = clone $this->markerFillColor;
$this->markerBorderColor = clone $this->markerBorderColor;
if (is_array($this->fillColor)) {
$fillColor = $this->fillColor;
$this->fillColor = [];
foreach ($fillColor as $color) {
$this->fillColor[] = clone $color;
}
} elseif ($this->fillColor instanceof ChartColor) {
$this->fillColor = clone $this->fillColor;
}
$this->labelLayout = ($this->labelLayout === null) ? null : clone $this->labelLayout;
$trendLines = $this->trendLines;
$this->trendLines = [];
foreach ($trendLines as $trendLine) {
$this->trendLines[] = clone $trendLine;
}
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
class Exception extends PhpSpreadsheetException
{
}

View file

@ -0,0 +1,13 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart;
/**
* Created by PhpStorm.
* User: Wiktor Trzonkowski
* Date: 7/2/14
* Time: 2:36 PM.
*/
class GridLines extends Properties
{
}

View file

@ -0,0 +1,524 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart;
use PhpOffice\PhpSpreadsheet\Style\Font;
class Layout
{
/**
* layoutTarget.
*/
private ?string $layoutTarget = null;
/**
* X Mode.
*/
private ?string $xMode = null;
/**
* Y Mode.
*/
private ?string $yMode = null;
/**
* X-Position.
*/
private ?float $xPos = null;
/**
* Y-Position.
*/
private ?float $yPos = null;
/**
* width.
*/
private ?float $width = null;
/**
* height.
*/
private ?float $height = null;
/**
* Position - t=top.
*/
private string $dLblPos = '';
private string $numFmtCode = '';
private bool $numFmtLinked = false;
/**
* show legend key
* Specifies that legend keys should be shown in data labels.
*/
private ?bool $showLegendKey = null;
/**
* show value
* Specifies that the value should be shown in a data label.
*/
private ?bool $showVal = null;
/**
* show category name
* Specifies that the category name should be shown in the data label.
*/
private ?bool $showCatName = null;
/**
* show data series name
* Specifies that the series name should be shown in the data label.
*/
private ?bool $showSerName = null;
/**
* show percentage
* Specifies that the percentage should be shown in the data label.
*/
private ?bool $showPercent = null;
/**
* show bubble size.
*/
private ?bool $showBubbleSize = null;
/**
* show leader lines
* Specifies that leader lines should be shown for the data label.
*/
private ?bool $showLeaderLines = null;
private ?ChartColor $labelFillColor = null;
private ?ChartColor $labelBorderColor = null;
private ?Font $labelFont = null;
private ?Properties $labelEffects = null;
/**
* Create a new Layout.
*/
public function __construct(array $layout = [])
{
if (isset($layout['layoutTarget'])) {
$this->layoutTarget = $layout['layoutTarget'];
}
if (isset($layout['xMode'])) {
$this->xMode = $layout['xMode'];
}
if (isset($layout['yMode'])) {
$this->yMode = $layout['yMode'];
}
if (isset($layout['x'])) {
$this->xPos = (float) $layout['x'];
}
if (isset($layout['y'])) {
$this->yPos = (float) $layout['y'];
}
if (isset($layout['w'])) {
$this->width = (float) $layout['w'];
}
if (isset($layout['h'])) {
$this->height = (float) $layout['h'];
}
if (isset($layout['dLblPos'])) {
$this->dLblPos = (string) $layout['dLblPos'];
}
if (isset($layout['numFmtCode'])) {
$this->numFmtCode = (string) $layout['numFmtCode'];
}
$this->initBoolean($layout, 'showLegendKey');
$this->initBoolean($layout, 'showVal');
$this->initBoolean($layout, 'showCatName');
$this->initBoolean($layout, 'showSerName');
$this->initBoolean($layout, 'showPercent');
$this->initBoolean($layout, 'showBubbleSize');
$this->initBoolean($layout, 'showLeaderLines');
$this->initBoolean($layout, 'numFmtLinked');
$this->initColor($layout, 'labelFillColor');
$this->initColor($layout, 'labelBorderColor');
$labelFont = $layout['labelFont'] ?? null;
if ($labelFont instanceof Font) {
$this->labelFont = $labelFont;
}
$labelFontColor = $layout['labelFontColor'] ?? null;
if ($labelFontColor instanceof ChartColor) {
$this->setLabelFontColor($labelFontColor);
}
$labelEffects = $layout['labelEffects'] ?? null;
if ($labelEffects instanceof Properties) {
$this->labelEffects = $labelEffects;
}
}
private function initBoolean(array $layout, string $name): void
{
if (isset($layout[$name])) {
$this->$name = (bool) $layout[$name];
}
}
private function initColor(array $layout, string $name): void
{
if (isset($layout[$name]) && $layout[$name] instanceof ChartColor) {
$this->$name = $layout[$name];
}
}
/**
* Get Layout Target.
*/
public function getLayoutTarget(): ?string
{
return $this->layoutTarget;
}
/**
* Set Layout Target.
*
* @return $this
*/
public function setLayoutTarget(?string $target): static
{
$this->layoutTarget = $target;
return $this;
}
/**
* Get X-Mode.
*/
public function getXMode(): ?string
{
return $this->xMode;
}
/**
* Set X-Mode.
*
* @return $this
*/
public function setXMode(?string $mode): static
{
$this->xMode = (string) $mode;
return $this;
}
/**
* Get Y-Mode.
*/
public function getYMode(): ?string
{
return $this->yMode;
}
/**
* Set Y-Mode.
*
* @return $this
*/
public function setYMode(?string $mode): static
{
$this->yMode = (string) $mode;
return $this;
}
/**
* Get X-Position.
*/
public function getXPosition(): null|float|int
{
return $this->xPos;
}
/**
* Set X-Position.
*
* @return $this
*/
public function setXPosition(float $position): static
{
$this->xPos = $position;
return $this;
}
/**
* Get Y-Position.
*/
public function getYPosition(): ?float
{
return $this->yPos;
}
/**
* Set Y-Position.
*
* @return $this
*/
public function setYPosition(float $position): static
{
$this->yPos = $position;
return $this;
}
/**
* Get Width.
*/
public function getWidth(): ?float
{
return $this->width;
}
/**
* Set Width.
*
* @return $this
*/
public function setWidth(?float $width): static
{
$this->width = $width;
return $this;
}
/**
* Get Height.
*/
public function getHeight(): ?float
{
return $this->height;
}
/**
* Set Height.
*
* @return $this
*/
public function setHeight(?float $height): static
{
$this->height = $height;
return $this;
}
public function getShowLegendKey(): ?bool
{
return $this->showLegendKey;
}
/**
* Set show legend key
* Specifies that legend keys should be shown in data labels.
*/
public function setShowLegendKey(?bool $showLegendKey): self
{
$this->showLegendKey = $showLegendKey;
return $this;
}
public function getShowVal(): ?bool
{
return $this->showVal;
}
/**
* Set show val
* Specifies that the value should be shown in data labels.
*/
public function setShowVal(?bool $showDataLabelValues): self
{
$this->showVal = $showDataLabelValues;
return $this;
}
public function getShowCatName(): ?bool
{
return $this->showCatName;
}
/**
* Set show cat name
* Specifies that the category name should be shown in data labels.
*/
public function setShowCatName(?bool $showCategoryName): self
{
$this->showCatName = $showCategoryName;
return $this;
}
public function getShowSerName(): ?bool
{
return $this->showSerName;
}
/**
* Set show data series name.
* Specifies that the series name should be shown in data labels.
*/
public function setShowSerName(?bool $showSeriesName): self
{
$this->showSerName = $showSeriesName;
return $this;
}
public function getShowPercent(): ?bool
{
return $this->showPercent;
}
/**
* Set show percentage.
* Specifies that the percentage should be shown in data labels.
*/
public function setShowPercent(?bool $showPercentage): self
{
$this->showPercent = $showPercentage;
return $this;
}
public function getShowBubbleSize(): ?bool
{
return $this->showBubbleSize;
}
/**
* Set show bubble size.
* Specifies that the bubble size should be shown in data labels.
*/
public function setShowBubbleSize(?bool $showBubbleSize): self
{
$this->showBubbleSize = $showBubbleSize;
return $this;
}
public function getShowLeaderLines(): ?bool
{
return $this->showLeaderLines;
}
/**
* Set show leader lines.
* Specifies that leader lines should be shown in data labels.
*/
public function setShowLeaderLines(?bool $showLeaderLines): self
{
$this->showLeaderLines = $showLeaderLines;
return $this;
}
public function getLabelFillColor(): ?ChartColor
{
return $this->labelFillColor;
}
public function setLabelFillColor(?ChartColor $chartColor): self
{
$this->labelFillColor = $chartColor;
return $this;
}
public function getLabelBorderColor(): ?ChartColor
{
return $this->labelBorderColor;
}
public function setLabelBorderColor(?ChartColor $chartColor): self
{
$this->labelBorderColor = $chartColor;
return $this;
}
public function getLabelFont(): ?Font
{
return $this->labelFont;
}
public function getLabelEffects(): ?Properties
{
return $this->labelEffects;
}
public function getLabelFontColor(): ?ChartColor
{
if ($this->labelFont === null) {
return null;
}
return $this->labelFont->getChartColor();
}
public function setLabelFontColor(?ChartColor $chartColor): self
{
if ($this->labelFont === null) {
$this->labelFont = new Font();
$this->labelFont->setSize(null, true);
}
$this->labelFont->setChartColorFromObject($chartColor);
return $this;
}
public function getDLblPos(): string
{
return $this->dLblPos;
}
public function setDLblPos(string $dLblPos): self
{
$this->dLblPos = $dLblPos;
return $this;
}
public function getNumFmtCode(): string
{
return $this->numFmtCode;
}
public function setNumFmtCode(string $numFmtCode): self
{
$this->numFmtCode = $numFmtCode;
return $this;
}
public function getNumFmtLinked(): bool
{
return $this->numFmtLinked;
}
public function setNumFmtLinked(bool $numFmtLinked): self
{
$this->numFmtLinked = $numFmtLinked;
return $this;
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$this->labelFillColor = ($this->labelFillColor === null) ? null : clone $this->labelFillColor;
$this->labelBorderColor = ($this->labelBorderColor === null) ? null : clone $this->labelBorderColor;
$this->labelFont = ($this->labelFont === null) ? null : clone $this->labelFont;
$this->labelEffects = ($this->labelEffects === null) ? null : clone $this->labelEffects;
}
}

View file

@ -0,0 +1,174 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart;
class Legend
{
/** Legend positions */
const XL_LEGEND_POSITION_BOTTOM = -4107; // Below the chart.
const XL_LEGEND_POSITION_CORNER = 2; // In the upper right-hand corner of the chart border.
const XL_LEGEND_POSITION_CUSTOM = -4161; // A custom position.
const XL_LEGEND_POSITION_LEFT = -4131; // Left of the chart.
const XL_LEGEND_POSITION_RIGHT = -4152; // Right of the chart.
const XL_LEGEND_POSITION_TOP = -4160; // Above the chart.
const POSITION_RIGHT = 'r';
const POSITION_LEFT = 'l';
const POSITION_BOTTOM = 'b';
const POSITION_TOP = 't';
const POSITION_TOPRIGHT = 'tr';
const POSITION_XLREF = [
self::XL_LEGEND_POSITION_BOTTOM => self::POSITION_BOTTOM,
self::XL_LEGEND_POSITION_CORNER => self::POSITION_TOPRIGHT,
self::XL_LEGEND_POSITION_CUSTOM => '??',
self::XL_LEGEND_POSITION_LEFT => self::POSITION_LEFT,
self::XL_LEGEND_POSITION_RIGHT => self::POSITION_RIGHT,
self::XL_LEGEND_POSITION_TOP => self::POSITION_TOP,
];
/**
* Legend position.
*/
private string $position = self::POSITION_RIGHT;
/**
* Allow overlay of other elements?
*/
private bool $overlay = true;
/**
* Legend Layout.
*/
private ?Layout $layout;
private GridLines $borderLines;
private ChartColor $fillColor;
private ?AxisText $legendText = null;
/**
* Create a new Legend.
*/
public function __construct(string $position = self::POSITION_RIGHT, ?Layout $layout = null, bool $overlay = false)
{
$this->setPosition($position);
$this->layout = $layout;
$this->setOverlay($overlay);
$this->borderLines = new GridLines();
$this->fillColor = new ChartColor();
}
public function getFillColor(): ChartColor
{
return $this->fillColor;
}
/**
* Get legend position as an excel string value.
*/
public function getPosition(): string
{
return $this->position;
}
/**
* Get legend position using an excel string value.
*
* @param string $position see self::POSITION_*
*/
public function setPosition(string $position): bool
{
if (!in_array($position, self::POSITION_XLREF)) {
return false;
}
$this->position = $position;
return true;
}
/**
* Get legend position as an Excel internal numeric value.
*/
public function getPositionXL(): false|int
{
return array_search($this->position, self::POSITION_XLREF);
}
/**
* Set legend position using an Excel internal numeric value.
*
* @param int $positionXL see self::XL_LEGEND_POSITION_*
*/
public function setPositionXL(int $positionXL): bool
{
if (!isset(self::POSITION_XLREF[$positionXL])) {
return false;
}
$this->position = self::POSITION_XLREF[$positionXL];
return true;
}
/**
* Get allow overlay of other elements?
*/
public function getOverlay(): bool
{
return $this->overlay;
}
/**
* Set allow overlay of other elements?
*/
public function setOverlay(bool $overlay): void
{
$this->overlay = $overlay;
}
/**
* Get Layout.
*/
public function getLayout(): ?Layout
{
return $this->layout;
}
public function getLegendText(): ?AxisText
{
return $this->legendText;
}
public function setLegendText(?AxisText $legendText): self
{
$this->legendText = $legendText;
return $this;
}
public function getBorderLines(): GridLines
{
return $this->borderLines;
}
public function setBorderLines(GridLines $borderLines): self
{
$this->borderLines = $borderLines;
return $this;
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$this->layout = ($this->layout === null) ? null : clone $this->layout;
$this->legendText = ($this->legendText === null) ? null : clone $this->legendText;
$this->borderLines = clone $this->borderLines;
$this->fillColor = clone $this->fillColor;
}
}

View file

@ -0,0 +1,207 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class PlotArea
{
/**
* No fill in plot area (show Excel gridlines through chart).
*/
private bool $noFill = false;
/**
* PlotArea Gradient Stop list.
* Each entry is a 2-element array.
* First is position in %.
* Second is ChartColor.
*
* @var array[]
*/
private array $gradientFillStops = [];
/**
* PlotArea Gradient Angle.
*/
private ?float $gradientFillAngle = null;
/**
* PlotArea Layout.
*/
private ?Layout $layout;
/**
* Plot Series.
*
* @var DataSeries[]
*/
private array $plotSeries;
/**
* Create a new PlotArea.
*
* @param DataSeries[] $plotSeries
*/
public function __construct(?Layout $layout = null, array $plotSeries = [])
{
$this->layout = $layout;
$this->plotSeries = $plotSeries;
}
public function getLayout(): ?Layout
{
return $this->layout;
}
/**
* Get Number of Plot Groups.
*/
public function getPlotGroupCount(): int
{
return count($this->plotSeries);
}
/**
* Get Number of Plot Series.
*/
public function getPlotSeriesCount(): int|float
{
$seriesCount = 0;
foreach ($this->plotSeries as $plot) {
$seriesCount += $plot->getPlotSeriesCount();
}
return $seriesCount;
}
/**
* Get Plot Series.
*
* @return DataSeries[]
*/
public function getPlotGroup(): array
{
return $this->plotSeries;
}
/**
* Get Plot Series by Index.
*/
public function getPlotGroupByIndex(mixed $index): DataSeries
{
return $this->plotSeries[$index];
}
/**
* Set Plot Series.
*
* @param DataSeries[] $plotSeries
*
* @return $this
*/
public function setPlotSeries(array $plotSeries): static
{
$this->plotSeries = $plotSeries;
return $this;
}
public function refresh(Worksheet $worksheet): void
{
foreach ($this->plotSeries as $plotSeries) {
$plotSeries->refresh($worksheet);
}
}
public function setNoFill(bool $noFill): self
{
$this->noFill = $noFill;
return $this;
}
public function getNoFill(): bool
{
return $this->noFill;
}
public function setGradientFillProperties(array $gradientFillStops, ?float $gradientFillAngle): self
{
$this->gradientFillStops = $gradientFillStops;
$this->gradientFillAngle = $gradientFillAngle;
return $this;
}
/**
* Get gradientFillAngle.
*/
public function getGradientFillAngle(): ?float
{
return $this->gradientFillAngle;
}
/**
* Get gradientFillStops.
*/
public function getGradientFillStops(): array
{
return $this->gradientFillStops;
}
private ?int $gapWidth = null;
private bool $useUpBars = false;
private bool $useDownBars = false;
public function getGapWidth(): ?int
{
return $this->gapWidth;
}
public function setGapWidth(?int $gapWidth): self
{
$this->gapWidth = $gapWidth;
return $this;
}
public function getUseUpBars(): bool
{
return $this->useUpBars;
}
public function setUseUpBars(bool $useUpBars): self
{
$this->useUpBars = $useUpBars;
return $this;
}
public function getUseDownBars(): bool
{
return $this->useDownBars;
}
public function setUseDownBars(bool $useDownBars): self
{
$this->useDownBars = $useDownBars;
return $this;
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$this->layout = ($this->layout === null) ? null : clone $this->layout;
$plotSeries = $this->plotSeries;
$this->plotSeries = [];
foreach ($plotSeries as $series) {
$this->plotSeries[] = clone $series;
}
}
}

View file

@ -0,0 +1,884 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart;
/**
* Created by PhpStorm.
* User: nhw2h8s
* Date: 7/2/14
* Time: 5:45 PM.
*/
abstract class Properties
{
const AXIS_LABELS_LOW = 'low';
const AXIS_LABELS_HIGH = 'high';
const AXIS_LABELS_NEXT_TO = 'nextTo';
const AXIS_LABELS_NONE = 'none';
const TICK_MARK_NONE = 'none';
const TICK_MARK_INSIDE = 'in';
const TICK_MARK_OUTSIDE = 'out';
const TICK_MARK_CROSS = 'cross';
const HORIZONTAL_CROSSES_AUTOZERO = 'autoZero';
const HORIZONTAL_CROSSES_MAXIMUM = 'max';
const FORMAT_CODE_GENERAL = 'General';
const FORMAT_CODE_NUMBER = '#,##0.00';
const FORMAT_CODE_CURRENCY = '$#,##0.00';
const FORMAT_CODE_ACCOUNTING = '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(@_)';
const FORMAT_CODE_DATE = 'm/d/yyyy';
const FORMAT_CODE_DATE_ISO8601 = 'yyyy-mm-dd';
const FORMAT_CODE_TIME = '[$-F400]h:mm:ss AM/PM';
const FORMAT_CODE_PERCENTAGE = '0.00%';
const FORMAT_CODE_FRACTION = '# ?/?';
const FORMAT_CODE_SCIENTIFIC = '0.00E+00';
const FORMAT_CODE_TEXT = '@';
const FORMAT_CODE_SPECIAL = '00000';
const ORIENTATION_NORMAL = 'minMax';
const ORIENTATION_REVERSED = 'maxMin';
const LINE_STYLE_COMPOUND_SIMPLE = 'sng';
const LINE_STYLE_COMPOUND_DOUBLE = 'dbl';
const LINE_STYLE_COMPOUND_THICKTHIN = 'thickThin';
const LINE_STYLE_COMPOUND_THINTHICK = 'thinThick';
const LINE_STYLE_COMPOUND_TRIPLE = 'tri';
const LINE_STYLE_DASH_SOLID = 'solid';
const LINE_STYLE_DASH_ROUND_DOT = 'sysDot';
const LINE_STYLE_DASH_SQUARE_DOT = 'sysDash';
const LINE_STYPE_DASH_DASH = 'dash';
const LINE_STYLE_DASH_DASH_DOT = 'dashDot';
const LINE_STYLE_DASH_LONG_DASH = 'lgDash';
const LINE_STYLE_DASH_LONG_DASH_DOT = 'lgDashDot';
const LINE_STYLE_DASH_LONG_DASH_DOT_DOT = 'lgDashDotDot';
const LINE_STYLE_CAP_SQUARE = 'sq';
const LINE_STYLE_CAP_ROUND = 'rnd';
const LINE_STYLE_CAP_FLAT = 'flat';
const LINE_STYLE_JOIN_ROUND = 'round';
const LINE_STYLE_JOIN_MITER = 'miter';
const LINE_STYLE_JOIN_BEVEL = 'bevel';
const LINE_STYLE_ARROW_TYPE_NOARROW = null;
const LINE_STYLE_ARROW_TYPE_ARROW = 'triangle';
const LINE_STYLE_ARROW_TYPE_OPEN = 'arrow';
const LINE_STYLE_ARROW_TYPE_STEALTH = 'stealth';
const LINE_STYLE_ARROW_TYPE_DIAMOND = 'diamond';
const LINE_STYLE_ARROW_TYPE_OVAL = 'oval';
const LINE_STYLE_ARROW_SIZE_1 = 1;
const LINE_STYLE_ARROW_SIZE_2 = 2;
const LINE_STYLE_ARROW_SIZE_3 = 3;
const LINE_STYLE_ARROW_SIZE_4 = 4;
const LINE_STYLE_ARROW_SIZE_5 = 5;
const LINE_STYLE_ARROW_SIZE_6 = 6;
const LINE_STYLE_ARROW_SIZE_7 = 7;
const LINE_STYLE_ARROW_SIZE_8 = 8;
const LINE_STYLE_ARROW_SIZE_9 = 9;
const SHADOW_PRESETS_NOSHADOW = null;
const SHADOW_PRESETS_OUTER_BOTTTOM_RIGHT = 1;
const SHADOW_PRESETS_OUTER_BOTTOM = 2;
const SHADOW_PRESETS_OUTER_BOTTOM_LEFT = 3;
const SHADOW_PRESETS_OUTER_RIGHT = 4;
const SHADOW_PRESETS_OUTER_CENTER = 5;
const SHADOW_PRESETS_OUTER_LEFT = 6;
const SHADOW_PRESETS_OUTER_TOP_RIGHT = 7;
const SHADOW_PRESETS_OUTER_TOP = 8;
const SHADOW_PRESETS_OUTER_TOP_LEFT = 9;
const SHADOW_PRESETS_INNER_BOTTTOM_RIGHT = 10;
const SHADOW_PRESETS_INNER_BOTTOM = 11;
const SHADOW_PRESETS_INNER_BOTTOM_LEFT = 12;
const SHADOW_PRESETS_INNER_RIGHT = 13;
const SHADOW_PRESETS_INNER_CENTER = 14;
const SHADOW_PRESETS_INNER_LEFT = 15;
const SHADOW_PRESETS_INNER_TOP_RIGHT = 16;
const SHADOW_PRESETS_INNER_TOP = 17;
const SHADOW_PRESETS_INNER_TOP_LEFT = 18;
const SHADOW_PRESETS_PERSPECTIVE_BELOW = 19;
const SHADOW_PRESETS_PERSPECTIVE_UPPER_RIGHT = 20;
const SHADOW_PRESETS_PERSPECTIVE_UPPER_LEFT = 21;
const SHADOW_PRESETS_PERSPECTIVE_LOWER_RIGHT = 22;
const SHADOW_PRESETS_PERSPECTIVE_LOWER_LEFT = 23;
const POINTS_WIDTH_MULTIPLIER = 12700;
const ANGLE_MULTIPLIER = 60000; // direction and size-kx size-ky
const PERCENTAGE_MULTIPLIER = 100000; // size sx and sy
protected bool $objectState = false; // used only for minor gridlines
/** @var ?float */
protected ?float $glowSize = null;
protected ChartColor $glowColor;
protected array $softEdges = [
'size' => null,
];
protected array $shadowProperties = self::PRESETS_OPTIONS[0];
protected ChartColor $shadowColor;
public function __construct()
{
$this->lineColor = new ChartColor();
$this->glowColor = new ChartColor();
$this->shadowColor = new ChartColor();
$this->shadowColor->setType(ChartColor::EXCEL_COLOR_TYPE_STANDARD);
$this->shadowColor->setValue('black');
$this->shadowColor->setAlpha(40);
}
/**
* Get Object State.
*/
public function getObjectState(): bool
{
return $this->objectState;
}
/**
* Change Object State to True.
*
* @return $this
*/
public function activateObject()
{
$this->objectState = true;
return $this;
}
public static function pointsToXml(float $width): string
{
return (string) (int) ($width * self::POINTS_WIDTH_MULTIPLIER);
}
public static function xmlToPoints(string $width): float
{
return ((float) $width) / self::POINTS_WIDTH_MULTIPLIER;
}
public static function angleToXml(float $angle): string
{
return (string) (int) ($angle * self::ANGLE_MULTIPLIER);
}
public static function xmlToAngle(string $angle): float
{
return ((float) $angle) / self::ANGLE_MULTIPLIER;
}
public static function tenthOfPercentToXml(float $value): string
{
return (string) (int) ($value * self::PERCENTAGE_MULTIPLIER);
}
public static function xmlToTenthOfPercent(string $value): float
{
return ((float) $value) / self::PERCENTAGE_MULTIPLIER;
}
protected function setColorProperties(?string $color, null|float|int|string $alpha, ?string $colorType): array
{
return [
'type' => $colorType,
'value' => $color,
'alpha' => ($alpha === null) ? null : (int) $alpha,
];
}
protected const PRESETS_OPTIONS = [
//NONE
0 => [
'presets' => self::SHADOW_PRESETS_NOSHADOW,
'effect' => null,
//'color' => [
// 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD,
// 'value' => 'black',
// 'alpha' => 40,
//],
'size' => [
'sx' => null,
'sy' => null,
'kx' => null,
'ky' => null,
],
'blur' => null,
'direction' => null,
'distance' => null,
'algn' => null,
'rotWithShape' => null,
],
//OUTER
1 => [
'effect' => 'outerShdw',
'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 2700000 / self::ANGLE_MULTIPLIER,
'algn' => 'tl',
'rotWithShape' => '0',
],
2 => [
'effect' => 'outerShdw',
'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 5400000 / self::ANGLE_MULTIPLIER,
'algn' => 't',
'rotWithShape' => '0',
],
3 => [
'effect' => 'outerShdw',
'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 8100000 / self::ANGLE_MULTIPLIER,
'algn' => 'tr',
'rotWithShape' => '0',
],
4 => [
'effect' => 'outerShdw',
'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER,
'algn' => 'l',
'rotWithShape' => '0',
],
5 => [
'effect' => 'outerShdw',
'size' => [
'sx' => 102000 / self::PERCENTAGE_MULTIPLIER,
'sy' => 102000 / self::PERCENTAGE_MULTIPLIER,
],
'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER,
'algn' => 'ctr',
'rotWithShape' => '0',
],
6 => [
'effect' => 'outerShdw',
'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 10800000 / self::ANGLE_MULTIPLIER,
'algn' => 'r',
'rotWithShape' => '0',
],
7 => [
'effect' => 'outerShdw',
'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 18900000 / self::ANGLE_MULTIPLIER,
'algn' => 'bl',
'rotWithShape' => '0',
],
8 => [
'effect' => 'outerShdw',
'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 16200000 / self::ANGLE_MULTIPLIER,
'rotWithShape' => '0',
],
9 => [
'effect' => 'outerShdw',
'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 13500000 / self::ANGLE_MULTIPLIER,
'algn' => 'br',
'rotWithShape' => '0',
],
//INNER
10 => [
'effect' => 'innerShdw',
'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 2700000 / self::ANGLE_MULTIPLIER,
],
11 => [
'effect' => 'innerShdw',
'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 5400000 / self::ANGLE_MULTIPLIER,
],
12 => [
'effect' => 'innerShdw',
'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 8100000 / self::ANGLE_MULTIPLIER,
],
13 => [
'effect' => 'innerShdw',
'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
],
14 => [
'effect' => 'innerShdw',
'blur' => 114300 / self::POINTS_WIDTH_MULTIPLIER,
],
15 => [
'effect' => 'innerShdw',
'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 10800000 / self::ANGLE_MULTIPLIER,
],
16 => [
'effect' => 'innerShdw',
'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 18900000 / self::ANGLE_MULTIPLIER,
],
17 => [
'effect' => 'innerShdw',
'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 16200000 / self::ANGLE_MULTIPLIER,
],
18 => [
'effect' => 'innerShdw',
'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 13500000 / self::ANGLE_MULTIPLIER,
],
//perspective
19 => [
'effect' => 'outerShdw',
'blur' => 152400 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 317500 / self::POINTS_WIDTH_MULTIPLIER,
'size' => [
'sx' => 90000 / self::PERCENTAGE_MULTIPLIER,
'sy' => -19000 / self::PERCENTAGE_MULTIPLIER,
],
'direction' => 5400000 / self::ANGLE_MULTIPLIER,
'rotWithShape' => '0',
],
20 => [
'effect' => 'outerShdw',
'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 18900000 / self::ANGLE_MULTIPLIER,
'size' => [
'sy' => 23000 / self::PERCENTAGE_MULTIPLIER,
'kx' => -1200000 / self::ANGLE_MULTIPLIER,
],
'algn' => 'bl',
'rotWithShape' => '0',
],
21 => [
'effect' => 'outerShdw',
'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 13500000 / self::ANGLE_MULTIPLIER,
'size' => [
'sy' => 23000 / self::PERCENTAGE_MULTIPLIER,
'kx' => 1200000 / self::ANGLE_MULTIPLIER,
],
'algn' => 'br',
'rotWithShape' => '0',
],
22 => [
'effect' => 'outerShdw',
'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 12700 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 2700000 / self::ANGLE_MULTIPLIER,
'size' => [
'sy' => -23000 / self::PERCENTAGE_MULTIPLIER,
'kx' => -800400 / self::ANGLE_MULTIPLIER,
],
'algn' => 'bl',
'rotWithShape' => '0',
],
23 => [
'effect' => 'outerShdw',
'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER,
'distance' => 12700 / self::POINTS_WIDTH_MULTIPLIER,
'direction' => 8100000 / self::ANGLE_MULTIPLIER,
'size' => [
'sy' => -23000 / self::PERCENTAGE_MULTIPLIER,
'kx' => 800400 / self::ANGLE_MULTIPLIER,
],
'algn' => 'br',
'rotWithShape' => '0',
],
];
protected function getShadowPresetsMap(int $presetsOption): array
{
return self::PRESETS_OPTIONS[$presetsOption] ?? self::PRESETS_OPTIONS[0];
}
/**
* Get value of array element.
*/
protected function getArrayElementsValue(mixed $properties, mixed $elements): mixed
{
$reference = &$properties;
if (!is_array($elements)) {
return $reference[$elements];
}
foreach ($elements as $keys) {
$reference = &$reference[$keys];
}
return $reference;
}
/**
* Set Glow Properties.
*/
public function setGlowProperties(float $size, ?string $colorValue = null, ?int $colorAlpha = null, ?string $colorType = null): void
{
$this
->activateObject()
->setGlowSize($size);
$this->glowColor->setColorPropertiesArray(
[
'value' => $colorValue,
'type' => $colorType,
'alpha' => $colorAlpha,
]
);
}
/**
* Get Glow Property.
*/
public function getGlowProperty(array|string $property): null|array|float|int|string
{
$retVal = null;
if ($property === 'size') {
$retVal = $this->glowSize;
} elseif ($property === 'color') {
$retVal = [
'value' => $this->glowColor->getColorProperty('value'),
'type' => $this->glowColor->getColorProperty('type'),
'alpha' => $this->glowColor->getColorProperty('alpha'),
];
} elseif (is_array($property) && count($property) >= 2 && $property[0] === 'color') {
$retVal = $this->glowColor->getColorProperty($property[1]);
}
return $retVal;
}
/**
* Get Glow Color Property.
*/
public function getGlowColor(string $propertyName): null|int|string
{
return $this->glowColor->getColorProperty($propertyName);
}
public function getGlowColorObject(): ChartColor
{
return $this->glowColor;
}
/**
* Get Glow Size.
*/
public function getGlowSize(): ?float
{
return $this->glowSize;
}
/**
* Set Glow Size.
*
* @return $this
*/
protected function setGlowSize(?float $size)
{
$this->glowSize = $size;
return $this;
}
/**
* Set Soft Edges Size.
*/
public function setSoftEdges(?float $size): void
{
if ($size !== null) {
$this->activateObject();
$this->softEdges['size'] = $size;
}
}
/**
* Get Soft Edges Size.
*/
public function getSoftEdgesSize(): ?float
{
return $this->softEdges['size'];
}
public function setShadowProperty(string $propertyName, mixed $value): self
{
$this->activateObject();
if ($propertyName === 'color' && is_array($value)) {
$this->shadowColor->setColorPropertiesArray($value);
} else {
$this->shadowProperties[$propertyName] = $value;
}
return $this;
}
/**
* Set Shadow Properties.
*/
public function setShadowProperties(int $presets, ?string $colorValue = null, ?string $colorType = null, null|float|int|string $colorAlpha = null, ?float $blur = null, ?int $angle = null, ?float $distance = null): void
{
$this->activateObject()->setShadowPresetsProperties((int) $presets);
if ($presets === 0) {
$this->shadowColor->setType(ChartColor::EXCEL_COLOR_TYPE_STANDARD);
$this->shadowColor->setValue('black');
$this->shadowColor->setAlpha(40);
}
if ($colorValue !== null) {
$this->shadowColor->setValue($colorValue);
}
if ($colorType !== null) {
$this->shadowColor->setType($colorType);
}
if (is_numeric($colorAlpha)) {
$this->shadowColor->setAlpha((int) $colorAlpha);
}
$this
->setShadowBlur($blur)
->setShadowAngle($angle)
->setShadowDistance($distance);
}
/**
* Set Shadow Presets Properties.
*
* @return $this
*/
protected function setShadowPresetsProperties(int $presets)
{
$this->shadowProperties['presets'] = $presets;
$this->setShadowPropertiesMapValues($this->getShadowPresetsMap($presets));
return $this;
}
protected const SHADOW_ARRAY_KEYS = ['size', 'color'];
/**
* Set Shadow Properties Values.
*
* @return $this
*/
protected function setShadowPropertiesMapValues(array $propertiesMap, ?array &$reference = null)
{
$base_reference = $reference;
foreach ($propertiesMap as $property_key => $property_val) {
if (is_array($property_val)) {
if (in_array($property_key, self::SHADOW_ARRAY_KEYS, true)) {
$reference = &$this->shadowProperties[$property_key];
$this->setShadowPropertiesMapValues($property_val, $reference);
}
} else {
if ($base_reference === null) {
$this->shadowProperties[$property_key] = $property_val;
} else {
$reference[$property_key] = $property_val;
}
}
}
return $this;
}
/**
* Set Shadow Blur.
*
* @return $this
*/
protected function setShadowBlur(?float $blur)
{
if ($blur !== null) {
$this->shadowProperties['blur'] = $blur;
}
return $this;
}
/**
* Set Shadow Angle.
*
* @return $this
*/
protected function setShadowAngle(null|float|int|string $angle)
{
if (is_numeric($angle)) {
$this->shadowProperties['direction'] = $angle;
}
return $this;
}
/**
* Set Shadow Distance.
*
* @return $this
*/
protected function setShadowDistance(?float $distance)
{
if ($distance !== null) {
$this->shadowProperties['distance'] = $distance;
}
return $this;
}
public function getShadowColorObject(): ChartColor
{
return $this->shadowColor;
}
/**
* Get Shadow Property.
*
* @param string|string[] $elements
*/
public function getShadowProperty($elements): array|string|null
{
if ($elements === 'color') {
return [
'value' => $this->shadowColor->getValue(),
'type' => $this->shadowColor->getType(),
'alpha' => $this->shadowColor->getAlpha(),
];
}
return $this->getArrayElementsValue($this->shadowProperties, $elements);
}
public function getShadowArray(): array
{
$array = $this->shadowProperties;
if ($this->getShadowColorObject()->isUsable()) {
$array['color'] = $this->getShadowProperty('color');
}
return $array;
}
protected ChartColor $lineColor;
protected array $lineStyleProperties = [
'width' => null, //'9525',
'compound' => '', //self::LINE_STYLE_COMPOUND_SIMPLE,
'dash' => '', //self::LINE_STYLE_DASH_SOLID,
'cap' => '', //self::LINE_STYLE_CAP_FLAT,
'join' => '', //self::LINE_STYLE_JOIN_BEVEL,
'arrow' => [
'head' => [
'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW,
'size' => '', //self::LINE_STYLE_ARROW_SIZE_5,
'w' => '',
'len' => '',
],
'end' => [
'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW,
'size' => '', //self::LINE_STYLE_ARROW_SIZE_8,
'w' => '',
'len' => '',
],
],
];
public function copyLineStyles(self $otherProperties): void
{
$this->lineStyleProperties = $otherProperties->lineStyleProperties;
$this->lineColor = $otherProperties->lineColor;
$this->glowSize = $otherProperties->glowSize;
$this->glowColor = $otherProperties->glowColor;
$this->softEdges = $otherProperties->softEdges;
$this->shadowProperties = $otherProperties->shadowProperties;
}
public function getLineColor(): ChartColor
{
return $this->lineColor;
}
/**
* Set Line Color Properties.
*/
public function setLineColorProperties(?string $value, ?int $alpha = null, ?string $colorType = null): void
{
$this->activateObject();
$this->lineColor->setColorPropertiesArray(
$this->setColorProperties(
$value,
$alpha,
$colorType
)
);
}
/**
* Get Line Color Property.
*/
public function getLineColorProperty(string $propertyName): null|int|string
{
return $this->lineColor->getColorProperty($propertyName);
}
/**
* Set Line Style Properties.
*/
public function setLineStyleProperties(
null|float|int|string $lineWidth = null,
?string $compoundType = '',
?string $dashType = '',
?string $capType = '',
?string $joinType = '',
?string $headArrowType = '',
int $headArrowSize = 0,
?string $endArrowType = '',
int $endArrowSize = 0,
?string $headArrowWidth = '',
?string $headArrowLength = '',
?string $endArrowWidth = '',
?string $endArrowLength = ''
): void {
$this->activateObject();
if (is_numeric($lineWidth)) {
$this->lineStyleProperties['width'] = $lineWidth;
}
if ($compoundType !== '') {
$this->lineStyleProperties['compound'] = $compoundType;
}
if ($dashType !== '') {
$this->lineStyleProperties['dash'] = $dashType;
}
if ($capType !== '') {
$this->lineStyleProperties['cap'] = $capType;
}
if ($joinType !== '') {
$this->lineStyleProperties['join'] = $joinType;
}
if ($headArrowType !== '') {
$this->lineStyleProperties['arrow']['head']['type'] = $headArrowType;
}
if (isset(self::ARROW_SIZES[$headArrowSize])) {
$this->lineStyleProperties['arrow']['head']['size'] = $headArrowSize;
$this->lineStyleProperties['arrow']['head']['w'] = self::ARROW_SIZES[$headArrowSize]['w'];
$this->lineStyleProperties['arrow']['head']['len'] = self::ARROW_SIZES[$headArrowSize]['len'];
}
if ($endArrowType !== '') {
$this->lineStyleProperties['arrow']['end']['type'] = $endArrowType;
}
if (isset(self::ARROW_SIZES[$endArrowSize])) {
$this->lineStyleProperties['arrow']['end']['size'] = $endArrowSize;
$this->lineStyleProperties['arrow']['end']['w'] = self::ARROW_SIZES[$endArrowSize]['w'];
$this->lineStyleProperties['arrow']['end']['len'] = self::ARROW_SIZES[$endArrowSize]['len'];
}
if ($headArrowWidth !== '') {
$this->lineStyleProperties['arrow']['head']['w'] = $headArrowWidth;
}
if ($headArrowLength !== '') {
$this->lineStyleProperties['arrow']['head']['len'] = $headArrowLength;
}
if ($endArrowWidth !== '') {
$this->lineStyleProperties['arrow']['end']['w'] = $endArrowWidth;
}
if ($endArrowLength !== '') {
$this->lineStyleProperties['arrow']['end']['len'] = $endArrowLength;
}
}
public function getLineStyleArray(): array
{
return $this->lineStyleProperties;
}
public function setLineStyleArray(array $lineStyleProperties = []): self
{
$this->activateObject();
$this->lineStyleProperties['width'] = $lineStyleProperties['width'] ?? null;
$this->lineStyleProperties['compound'] = $lineStyleProperties['compound'] ?? '';
$this->lineStyleProperties['dash'] = $lineStyleProperties['dash'] ?? '';
$this->lineStyleProperties['cap'] = $lineStyleProperties['cap'] ?? '';
$this->lineStyleProperties['join'] = $lineStyleProperties['join'] ?? '';
$this->lineStyleProperties['arrow']['head']['type'] = $lineStyleProperties['arrow']['head']['type'] ?? '';
$this->lineStyleProperties['arrow']['head']['size'] = $lineStyleProperties['arrow']['head']['size'] ?? '';
$this->lineStyleProperties['arrow']['head']['w'] = $lineStyleProperties['arrow']['head']['w'] ?? '';
$this->lineStyleProperties['arrow']['head']['len'] = $lineStyleProperties['arrow']['head']['len'] ?? '';
$this->lineStyleProperties['arrow']['end']['type'] = $lineStyleProperties['arrow']['end']['type'] ?? '';
$this->lineStyleProperties['arrow']['end']['size'] = $lineStyleProperties['arrow']['end']['size'] ?? '';
$this->lineStyleProperties['arrow']['end']['w'] = $lineStyleProperties['arrow']['end']['w'] ?? '';
$this->lineStyleProperties['arrow']['end']['len'] = $lineStyleProperties['arrow']['end']['len'] ?? '';
return $this;
}
public function setLineStyleProperty(string $propertyName, mixed $value): self
{
$this->activateObject();
$this->lineStyleProperties[$propertyName] = $value;
return $this;
}
/**
* Get Line Style Property.
*/
public function getLineStyleProperty(array|string $elements): ?string
{
return $this->getArrayElementsValue($this->lineStyleProperties, $elements);
}
protected const ARROW_SIZES = [
1 => ['w' => 'sm', 'len' => 'sm'],
2 => ['w' => 'sm', 'len' => 'med'],
3 => ['w' => 'sm', 'len' => 'lg'],
4 => ['w' => 'med', 'len' => 'sm'],
5 => ['w' => 'med', 'len' => 'med'],
6 => ['w' => 'med', 'len' => 'lg'],
7 => ['w' => 'lg', 'len' => 'sm'],
8 => ['w' => 'lg', 'len' => 'med'],
9 => ['w' => 'lg', 'len' => 'lg'],
];
/**
* Get Line Style Arrow Size.
*/
protected function getLineStyleArrowSize(int $arraySelector, string $arrayKaySelector): string
{
return self::ARROW_SIZES[$arraySelector][$arrayKaySelector] ?? '';
}
/**
* Get Line Style Arrow Parameters.
*/
public function getLineStyleArrowParameters(string $arrowSelector, string $propertySelector): string
{
return $this->getLineStyleArrowSize($this->lineStyleProperties['arrow'][$arrowSelector]['size'], $propertySelector);
}
/**
* Get Line Style Arrow Width.
*/
public function getLineStyleArrowWidth(string $arrow): ?string
{
return $this->getLineStyleProperty(['arrow', $arrow, 'w']);
}
/**
* Get Line Style Arrow Excel Length.
*/
public function getLineStyleArrowLength(string $arrow): ?string
{
return $this->getLineStyleProperty(['arrow', $arrow, 'len']);
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$this->lineColor = clone $this->lineColor;
$this->glowColor = clone $this->glowColor;
$this->shadowColor = clone $this->shadowColor;
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart\Renderer;
use PhpOffice\PhpSpreadsheet\Chart\Chart;
interface IRenderer
{
/**
* IRenderer constructor.
*/
public function __construct(Chart $chart);
/**
* Render the chart to given file (or stream).
*
* @param ?string $filename Name of the file render to
*
* @return bool true on success
*/
public function render(?string $filename): bool;
}

View file

@ -0,0 +1,40 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart\Renderer;
/**
* Jpgraph is not oficially maintained in Composer, so the version there
* could be out of date. For that reason, all unit test requiring Jpgraph
* are skipped. So, do not measure code coverage for this class till that
* is fixed.
*
* This implementation uses abandoned package
* https://packagist.org/packages/jpgraph/jpgraph
*
* @codeCoverageIgnore
*/
class JpGraph extends JpGraphRendererBase
{
protected static function init(): void
{
static $loaded = false;
if ($loaded) {
return;
}
// JpGraph is no longer included with distribution, but user may install it.
// So Scrutinizer's complaint that it can't find it is reasonable, but unfixable.
\JpGraph\JpGraph::load();
\JpGraph\JpGraph::module('bar');
\JpGraph\JpGraph::module('contour');
\JpGraph\JpGraph::module('line');
\JpGraph\JpGraph::module('pie');
\JpGraph\JpGraph::module('pie3d');
\JpGraph\JpGraph::module('radar');
\JpGraph\JpGraph::module('regstat');
\JpGraph\JpGraph::module('scatter');
\JpGraph\JpGraph::module('stock');
$loaded = true;
}
}

View file

@ -0,0 +1,882 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart\Renderer;
use AccBarPlot;
use AccLinePlot;
use BarPlot;
use ContourPlot;
use Graph;
use GroupBarPlot;
use LinePlot;
use PhpOffice\PhpSpreadsheet\Chart\Chart;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PieGraph;
use PiePlot;
use PiePlot3D;
use PiePlotC;
use RadarGraph;
use RadarPlot;
use ScatterPlot;
use Spline;
use StockPlot;
/**
* Base class for different Jpgraph implementations as charts renderer.
*/
abstract class JpGraphRendererBase implements IRenderer
{
private const DEFAULT_WIDTH = 640.0;
private const DEFAULT_HEIGHT = 480.0;
private static $colourSet = [
'mediumpurple1', 'palegreen3', 'gold1', 'cadetblue1',
'darkmagenta', 'coral', 'dodgerblue3', 'eggplant',
'mediumblue', 'magenta', 'sandybrown', 'cyan',
'firebrick1', 'forestgreen', 'deeppink4', 'darkolivegreen',
'goldenrod2',
];
private static array $markSet;
private Chart $chart;
private $graph;
private static $plotColour = 0;
private static $plotMark = 0;
/**
* Create a new jpgraph.
*/
public function __construct(Chart $chart)
{
static::init();
$this->graph = null;
$this->chart = $chart;
self::$markSet = [
'diamond' => MARK_DIAMOND,
'square' => MARK_SQUARE,
'triangle' => MARK_UTRIANGLE,
'x' => MARK_X,
'star' => MARK_STAR,
'dot' => MARK_FILLEDCIRCLE,
'dash' => MARK_DTRIANGLE,
'circle' => MARK_CIRCLE,
'plus' => MARK_CROSS,
];
}
private function getGraphWidth(): float
{
return $this->chart->getRenderedWidth() ?? self::DEFAULT_WIDTH;
}
private function getGraphHeight(): float
{
return $this->chart->getRenderedHeight() ?? self::DEFAULT_HEIGHT;
}
/**
* This method should be overriden in descendants to do real JpGraph library initialization.
*/
abstract protected static function init(): void;
private function formatPointMarker($seriesPlot, $markerID)
{
$plotMarkKeys = array_keys(self::$markSet);
if ($markerID === null) {
// Use default plot marker (next marker in the series)
self::$plotMark %= count(self::$markSet);
$seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]);
} elseif ($markerID !== 'none') {
// Use specified plot marker (if it exists)
if (isset(self::$markSet[$markerID])) {
$seriesPlot->mark->SetType(self::$markSet[$markerID]);
} else {
// If the specified plot marker doesn't exist, use default plot marker (next marker in the series)
self::$plotMark %= count(self::$markSet);
$seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]);
}
} else {
// Hide plot marker
$seriesPlot->mark->Hide();
}
$seriesPlot->mark->SetColor(self::$colourSet[self::$plotColour]);
$seriesPlot->mark->SetFillColor(self::$colourSet[self::$plotColour]);
$seriesPlot->SetColor(self::$colourSet[self::$plotColour++]);
return $seriesPlot;
}
private function formatDataSetLabels(int $groupID, array $datasetLabels, $rotation = '')
{
$datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode() ?? '';
// Retrieve any label formatting code
$datasetLabelFormatCode = stripslashes($datasetLabelFormatCode);
$testCurrentIndex = 0;
foreach ($datasetLabels as $i => $datasetLabel) {
if (is_array($datasetLabel)) {
if ($rotation == 'bar') {
$datasetLabels[$i] = implode(' ', $datasetLabel);
} else {
$datasetLabel = array_reverse($datasetLabel);
$datasetLabels[$i] = implode("\n", $datasetLabel);
}
} else {
// Format labels according to any formatting code
if ($datasetLabelFormatCode !== null) {
$datasetLabels[$i] = NumberFormat::toFormattedString($datasetLabel, $datasetLabelFormatCode);
}
}
++$testCurrentIndex;
}
return $datasetLabels;
}
private function percentageSumCalculation(int $groupID, $seriesCount)
{
$sumValues = [];
// Adjust our values to a percentage value across all series in the group
for ($i = 0; $i < $seriesCount; ++$i) {
if ($i == 0) {
$sumValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
} else {
$nextValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
foreach ($nextValues as $k => $value) {
if (isset($sumValues[$k])) {
$sumValues[$k] += $value;
} else {
$sumValues[$k] = $value;
}
}
}
}
return $sumValues;
}
private function percentageAdjustValues(array $dataValues, array $sumValues)
{
foreach ($dataValues as $k => $dataValue) {
$dataValues[$k] = $dataValue / $sumValues[$k] * 100;
}
return $dataValues;
}
private function getCaption($captionElement)
{
// Read any caption
$caption = ($captionElement !== null) ? $captionElement->getCaption() : null;
// Test if we have a title caption to display
if ($caption !== null) {
// If we do, it could be a plain string or an array
if (is_array($caption)) {
// Implode an array to a plain string
$caption = implode('', $caption);
}
}
return $caption;
}
private function renderTitle(): void
{
$title = $this->getCaption($this->chart->getTitle());
if ($title !== null) {
$this->graph->title->Set($title);
}
}
private function renderLegend(): void
{
$legend = $this->chart->getLegend();
if ($legend !== null) {
$legendPosition = $legend->getPosition();
switch ($legendPosition) {
case 'r':
$this->graph->legend->SetPos(0.01, 0.5, 'right', 'center'); // right
$this->graph->legend->SetColumns(1);
break;
case 'l':
$this->graph->legend->SetPos(0.01, 0.5, 'left', 'center'); // left
$this->graph->legend->SetColumns(1);
break;
case 't':
$this->graph->legend->SetPos(0.5, 0.01, 'center', 'top'); // top
break;
case 'b':
$this->graph->legend->SetPos(0.5, 0.99, 'center', 'bottom'); // bottom
break;
default:
$this->graph->legend->SetPos(0.01, 0.01, 'right', 'top'); // top-right
$this->graph->legend->SetColumns(1);
break;
}
} else {
$this->graph->legend->Hide();
}
}
private function renderCartesianPlotArea(string $type = 'textlin'): void
{
$this->graph = new Graph($this->getGraphWidth(), $this->getGraphHeight());
$this->graph->SetScale($type);
$this->renderTitle();
// Rotate for bar rather than column chart
$rotation = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotDirection();
$reverse = $rotation == 'bar';
$xAxisLabel = $this->chart->getXAxisLabel();
if ($xAxisLabel !== null) {
$title = $this->getCaption($xAxisLabel);
if ($title !== null) {
$this->graph->xaxis->SetTitle($title, 'center');
$this->graph->xaxis->title->SetMargin(35);
if ($reverse) {
$this->graph->xaxis->title->SetAngle(90);
$this->graph->xaxis->title->SetMargin(90);
}
}
}
$yAxisLabel = $this->chart->getYAxisLabel();
if ($yAxisLabel !== null) {
$title = $this->getCaption($yAxisLabel);
if ($title !== null) {
$this->graph->yaxis->SetTitle($title, 'center');
if ($reverse) {
$this->graph->yaxis->title->SetAngle(0);
$this->graph->yaxis->title->SetMargin(-55);
}
}
}
}
private function renderPiePlotArea(): void
{
$this->graph = new PieGraph($this->getGraphWidth(), $this->getGraphHeight());
$this->renderTitle();
}
private function renderRadarPlotArea(): void
{
$this->graph = new RadarGraph($this->getGraphWidth(), $this->getGraphHeight());
$this->graph->SetScale('lin');
$this->renderTitle();
}
private function renderPlotLine(int $groupID, bool $filled = false, bool $combination = false): void
{
$grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping();
$index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[0];
$labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount();
if ($labelCount > 0) {
$datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues();
$datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels);
$this->graph->xaxis->SetTickLabels($datasetLabels);
}
$seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
$seriesPlots = [];
if ($grouping == 'percentStacked') {
$sumValues = $this->percentageSumCalculation($groupID, $seriesCount);
} else {
$sumValues = [];
}
// Loop through each data series in turn
for ($i = 0; $i < $seriesCount; ++$i) {
$index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[$i];
$dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getDataValues();
$marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointMarker();
if ($grouping == 'percentStacked') {
$dataValues = $this->percentageAdjustValues($dataValues, $sumValues);
}
// Fill in any missing values in the $dataValues array
$testCurrentIndex = 0;
foreach ($dataValues as $k => $dataValue) {
while ($k != $testCurrentIndex) {
$dataValues[$testCurrentIndex] = null;
++$testCurrentIndex;
}
++$testCurrentIndex;
}
$seriesPlot = new LinePlot($dataValues);
if ($combination) {
$seriesPlot->SetBarCenter();
}
if ($filled) {
$seriesPlot->SetFilled(true);
$seriesPlot->SetColor('black');
$seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]);
} else {
// Set the appropriate plot marker
$this->formatPointMarker($seriesPlot, $marker);
}
$dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($index)->getDataValue();
$seriesPlot->SetLegend($dataLabel);
$seriesPlots[] = $seriesPlot;
}
if ($grouping == 'standard') {
$groupPlot = $seriesPlots;
} else {
$groupPlot = new AccLinePlot($seriesPlots);
}
$this->graph->Add($groupPlot);
}
private function renderPlotBar(int $groupID, ?string $dimensions = '2d'): void
{
$rotation = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotDirection();
// Rotate for bar rather than column chart
if (($groupID == 0) && ($rotation == 'bar')) {
$this->graph->Set90AndMargin();
}
$grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping();
$index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[0];
$labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount();
if ($labelCount > 0) {
$datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues();
$datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $rotation);
// Rotate for bar rather than column chart
if ($rotation == 'bar') {
$datasetLabels = array_reverse($datasetLabels);
$this->graph->yaxis->SetPos('max');
$this->graph->yaxis->SetLabelAlign('center', 'top');
$this->graph->yaxis->SetLabelSide(SIDE_RIGHT);
}
$this->graph->xaxis->SetTickLabels($datasetLabels);
}
$seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
$seriesPlots = [];
if ($grouping == 'percentStacked') {
$sumValues = $this->percentageSumCalculation($groupID, $seriesCount);
} else {
$sumValues = [];
}
// Loop through each data series in turn
for ($j = 0; $j < $seriesCount; ++$j) {
$index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[$j];
$dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getDataValues();
if ($grouping == 'percentStacked') {
$dataValues = $this->percentageAdjustValues($dataValues, $sumValues);
}
// Fill in any missing values in the $dataValues array
$testCurrentIndex = 0;
foreach ($dataValues as $k => $dataValue) {
while ($k != $testCurrentIndex) {
$dataValues[$testCurrentIndex] = null;
++$testCurrentIndex;
}
++$testCurrentIndex;
}
// Reverse the $dataValues order for bar rather than column chart
if ($rotation == 'bar') {
$dataValues = array_reverse($dataValues);
}
$seriesPlot = new BarPlot($dataValues);
$seriesPlot->SetColor('black');
$seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]);
if ($dimensions == '3d') {
$seriesPlot->SetShadow();
}
if (!$this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)) {
$dataLabel = '';
} else {
$dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)->getDataValue();
}
$seriesPlot->SetLegend($dataLabel);
$seriesPlots[] = $seriesPlot;
}
// Reverse the plot order for bar rather than column chart
if (($rotation == 'bar') && ($grouping != 'percentStacked')) {
$seriesPlots = array_reverse($seriesPlots);
}
if ($grouping == 'clustered') {
$groupPlot = new GroupBarPlot($seriesPlots);
} elseif ($grouping == 'standard') {
$groupPlot = new GroupBarPlot($seriesPlots);
} else {
$groupPlot = new AccBarPlot($seriesPlots);
if ($dimensions == '3d') {
$groupPlot->SetShadow();
}
}
$this->graph->Add($groupPlot);
}
private function renderPlotScatter(int $groupID, bool $bubble): void
{
$scatterStyle = $bubbleSize = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle();
$seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
// Loop through each data series in turn
for ($i = 0; $i < $seriesCount; ++$i) {
$plotCategoryByIndex = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i);
if ($plotCategoryByIndex === false) {
$plotCategoryByIndex = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0);
}
$dataValuesY = $plotCategoryByIndex->getDataValues();
$dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
$redoDataValuesY = true;
if ($bubble) {
if (!$bubbleSize) {
$bubbleSize = '10';
}
$redoDataValuesY = false;
foreach ($dataValuesY as $dataValueY) {
if (!is_int($dataValueY) && !is_float($dataValueY)) {
$redoDataValuesY = true;
break;
}
}
}
if ($redoDataValuesY) {
foreach ($dataValuesY as $k => $dataValueY) {
$dataValuesY[$k] = $k;
}
}
$seriesPlot = new ScatterPlot($dataValuesX, $dataValuesY);
if ($scatterStyle == 'lineMarker') {
$seriesPlot->SetLinkPoints();
$seriesPlot->link->SetColor(self::$colourSet[self::$plotColour]);
} elseif ($scatterStyle == 'smoothMarker') {
$spline = new Spline($dataValuesY, $dataValuesX);
[$splineDataY, $splineDataX] = $spline->Get(count($dataValuesX) * $this->getGraphWidth() / 20);
$lplot = new LinePlot($splineDataX, $splineDataY);
$lplot->SetColor(self::$colourSet[self::$plotColour]);
$this->graph->Add($lplot);
}
if ($bubble) {
$this->formatPointMarker($seriesPlot, 'dot');
$seriesPlot->mark->SetColor('black');
$seriesPlot->mark->SetSize($bubbleSize);
} else {
$marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker();
$this->formatPointMarker($seriesPlot, $marker);
}
$dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue();
$seriesPlot->SetLegend($dataLabel);
$this->graph->Add($seriesPlot);
}
}
private function renderPlotRadar(int $groupID): void
{
$radarStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle();
$seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
// Loop through each data series in turn
for ($i = 0; $i < $seriesCount; ++$i) {
$dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues();
$dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
$marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker();
$dataValues = [];
foreach ($dataValuesY as $k => $dataValueY) {
$dataValues[$k] = is_array($dataValueY) ? implode(' ', array_reverse($dataValueY)) : $dataValueY;
}
$tmp = array_shift($dataValues);
$dataValues[] = $tmp;
$tmp = array_shift($dataValuesX);
$dataValuesX[] = $tmp;
$this->graph->SetTitles(array_reverse($dataValues));
$seriesPlot = new RadarPlot(array_reverse($dataValuesX));
$dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue();
$seriesPlot->SetColor(self::$colourSet[self::$plotColour++]);
if ($radarStyle == 'filled') {
$seriesPlot->SetFillColor(self::$colourSet[self::$plotColour]);
}
$this->formatPointMarker($seriesPlot, $marker);
$seriesPlot->SetLegend($dataLabel);
$this->graph->Add($seriesPlot);
}
}
private function renderPlotContour(int $groupID): void
{
$seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
$dataValues = [];
// Loop through each data series in turn
for ($i = 0; $i < $seriesCount; ++$i) {
$dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
$dataValues[$i] = $dataValuesX;
}
$seriesPlot = new ContourPlot($dataValues);
$this->graph->Add($seriesPlot);
}
private function renderPlotStock(int $groupID): void
{
$seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
$plotOrder = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder();
$dataValues = [];
// Loop through each data series in turn and build the plot arrays
foreach ($plotOrder as $i => $v) {
$dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v);
if ($dataValuesX === false) {
continue;
}
$dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v)->getDataValues();
foreach ($dataValuesX as $j => $dataValueX) {
$dataValues[$plotOrder[$i]][$j] = $dataValueX;
}
}
if (empty($dataValues)) {
return;
}
$dataValuesPlot = [];
// Flatten the plot arrays to a single dimensional array to work with jpgraph
$jMax = count($dataValues[0]);
for ($j = 0; $j < $jMax; ++$j) {
for ($i = 0; $i < $seriesCount; ++$i) {
$dataValuesPlot[] = $dataValues[$i][$j] ?? null;
}
}
// Set the x-axis labels
$labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount();
if ($labelCount > 0) {
$datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues();
$datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels);
$this->graph->xaxis->SetTickLabels($datasetLabels);
}
$seriesPlot = new StockPlot($dataValuesPlot);
$seriesPlot->SetWidth(20);
$this->graph->Add($seriesPlot);
}
private function renderAreaChart($groupCount): void
{
$this->renderCartesianPlotArea();
for ($i = 0; $i < $groupCount; ++$i) {
$this->renderPlotLine($i, true, false);
}
}
private function renderLineChart($groupCount): void
{
$this->renderCartesianPlotArea();
for ($i = 0; $i < $groupCount; ++$i) {
$this->renderPlotLine($i, false, false);
}
}
private function renderBarChart($groupCount, ?string $dimensions = '2d'): void
{
$this->renderCartesianPlotArea();
for ($i = 0; $i < $groupCount; ++$i) {
$this->renderPlotBar($i, $dimensions);
}
}
private function renderScatterChart($groupCount): void
{
$this->renderCartesianPlotArea('linlin');
for ($i = 0; $i < $groupCount; ++$i) {
$this->renderPlotScatter($i, false);
}
}
private function renderBubbleChart($groupCount): void
{
$this->renderCartesianPlotArea('linlin');
for ($i = 0; $i < $groupCount; ++$i) {
$this->renderPlotScatter($i, true);
}
}
private function renderPieChart($groupCount, ?string $dimensions = '2d', bool $doughnut = false, bool $multiplePlots = false): void
{
$this->renderPiePlotArea();
$iLimit = ($multiplePlots) ? $groupCount : 1;
for ($groupID = 0; $groupID < $iLimit; ++$groupID) {
$exploded = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle();
$datasetLabels = [];
if ($groupID == 0) {
$labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount();
if ($labelCount > 0) {
$datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues();
$datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels);
}
}
$seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
// For pie charts, we only display the first series: doughnut charts generally display all series
$jLimit = ($multiplePlots) ? $seriesCount : 1;
// Loop through each data series in turn
for ($j = 0; $j < $jLimit; ++$j) {
$dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues();
// Fill in any missing values in the $dataValues array
$testCurrentIndex = 0;
foreach ($dataValues as $k => $dataValue) {
while ($k != $testCurrentIndex) {
$dataValues[$testCurrentIndex] = null;
++$testCurrentIndex;
}
++$testCurrentIndex;
}
if ($dimensions == '3d') {
$seriesPlot = new PiePlot3D($dataValues);
} else {
if ($doughnut) {
$seriesPlot = new PiePlotC($dataValues);
} else {
$seriesPlot = new PiePlot($dataValues);
}
}
if ($multiplePlots) {
$seriesPlot->SetSize(($jLimit - $j) / ($jLimit * 4));
}
if ($doughnut && method_exists($seriesPlot, 'SetMidColor')) {
$seriesPlot->SetMidColor('white');
}
$seriesPlot->SetColor(self::$colourSet[self::$plotColour++]);
if (count($datasetLabels) > 0) {
$seriesPlot->SetLabels(array_fill(0, count($datasetLabels), ''));
}
if ($dimensions != '3d') {
$seriesPlot->SetGuideLines(false);
}
if ($j == 0) {
if ($exploded) {
$seriesPlot->ExplodeAll();
}
$seriesPlot->SetLegends($datasetLabels);
}
$this->graph->Add($seriesPlot);
}
}
}
private function renderRadarChart($groupCount): void
{
$this->renderRadarPlotArea();
for ($groupID = 0; $groupID < $groupCount; ++$groupID) {
$this->renderPlotRadar($groupID);
}
}
private function renderStockChart($groupCount): void
{
$this->renderCartesianPlotArea('intint');
for ($groupID = 0; $groupID < $groupCount; ++$groupID) {
$this->renderPlotStock($groupID);
}
}
private function renderContourChart($groupCount): void
{
$this->renderCartesianPlotArea('intint');
for ($i = 0; $i < $groupCount; ++$i) {
$this->renderPlotContour($i);
}
}
private function renderCombinationChart($groupCount, $outputDestination): bool
{
$this->renderCartesianPlotArea();
for ($i = 0; $i < $groupCount; ++$i) {
$dimensions = null;
$chartType = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType();
switch ($chartType) {
case 'area3DChart':
case 'areaChart':
$this->renderPlotLine($i, true, true);
break;
case 'bar3DChart':
$dimensions = '3d';
// no break
case 'barChart':
$this->renderPlotBar($i, $dimensions);
break;
case 'line3DChart':
case 'lineChart':
$this->renderPlotLine($i, false, true);
break;
case 'scatterChart':
$this->renderPlotScatter($i, false);
break;
case 'bubbleChart':
$this->renderPlotScatter($i, true);
break;
default:
$this->graph = null;
return false;
}
}
$this->renderLegend();
$this->graph->Stroke($outputDestination);
return true;
}
public function render(?string $outputDestination): bool
{
self::$plotColour = 0;
$groupCount = $this->chart->getPlotArea()->getPlotGroupCount();
$dimensions = null;
if ($groupCount == 1) {
$chartType = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType();
} else {
$chartTypes = [];
for ($i = 0; $i < $groupCount; ++$i) {
$chartTypes[] = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType();
}
$chartTypes = array_unique($chartTypes);
if (count($chartTypes) == 1) {
$chartType = array_pop($chartTypes);
} elseif (count($chartTypes) == 0) {
echo 'Chart is not yet implemented<br />';
return false;
} else {
return $this->renderCombinationChart($groupCount, $outputDestination);
}
}
switch ($chartType) {
case 'area3DChart':
$dimensions = '3d';
// no break
case 'areaChart':
$this->renderAreaChart($groupCount);
break;
case 'bar3DChart':
$dimensions = '3d';
// no break
case 'barChart':
$this->renderBarChart($groupCount, $dimensions);
break;
case 'line3DChart':
$dimensions = '3d';
// no break
case 'lineChart':
$this->renderLineChart($groupCount);
break;
case 'pie3DChart':
$dimensions = '3d';
// no break
case 'pieChart':
$this->renderPieChart($groupCount, $dimensions, false, false);
break;
case 'doughnut3DChart':
$dimensions = '3d';
// no break
case 'doughnutChart':
$this->renderPieChart($groupCount, $dimensions, true, true);
break;
case 'scatterChart':
$this->renderScatterChart($groupCount);
break;
case 'bubbleChart':
$this->renderBubbleChart($groupCount);
break;
case 'radarChart':
$this->renderRadarChart($groupCount);
break;
case 'surface3DChart':
case 'surfaceChart':
$this->renderContourChart($groupCount);
break;
case 'stockChart':
$this->renderStockChart($groupCount);
break;
default:
echo $chartType . ' is not yet implemented<br />';
return false;
}
$this->renderLegend();
$this->graph->Stroke($outputDestination);
return true;
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart\Renderer;
use mitoteam\jpgraph\MtJpGraph;
/**
* Jpgraph is not officially maintained by Composer at packagist.org.
*
* This renderer implementation uses package
* https://packagist.org/packages/mitoteam/jpgraph
*
* This package is up to date for June 2023 and has PHP 8.2 support.
*/
class MtJpGraphRenderer extends JpGraphRendererBase
{
protected static function init(): void
{
static $loaded = false;
if ($loaded) {
return;
}
MtJpGraph::load([
'bar',
'contour',
'line',
'pie',
'pie3d',
'radar',
'regstat',
'scatter',
'stock',
], true); // enable Extended mode
$loaded = true;
}
}

View file

@ -0,0 +1,23 @@
ChartDirector
https://www.advsofteng.com/cdphp.html
GraPHPite
http://graphpite.sourceforge.net/
JpGraph
https://jpgraph.net/
Used composer packages:
https://packagist.org/packages/jpgraph/jpgraph (\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph)
https://packagist.org/packages/mitoteam/jpgraph (\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer)
LibChart
https://naku.dohcrew.com/libchart/pages/introduction/
pChart
http://pchart.sourceforge.net/
TeeChart
https://www.steema.com/
PHPGraphLib
http://www.ebrueggeman.com/phpgraphlib

View file

@ -0,0 +1,171 @@
<?php
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.
*
* @var array<RichText|string>|RichText|string
*/
private array|RichText|string $caption;
/**
* Allow overlay of other elements?
*/
private bool $overlay = true;
/**
* Title Layout.
*/
private ?Layout $layout;
private string $cellReference = '';
private ?Font $font = null;
/**
* Create a new Title.
*/
public function __construct(array|RichText|string $caption = '', ?Layout $layout = null, bool $overlay = false)
{
$this->caption = $caption;
$this->layout = $layout;
$this->setOverlay($overlay);
}
/**
* Get caption.
*/
public function getCaption(): array|RichText|string
{
return $this->caption;
}
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;
}
if ($caption instanceof RichText) {
return $caption->getPlainText();
}
$retVal = '';
foreach ($caption as $textx) {
/** @var RichText|string $text */
$text = $textx;
if ($text instanceof RichText) {
$retVal .= $text->getPlainText();
} else {
$retVal .= $text;
}
}
return $retVal;
}
/**
* Set caption.
*
* @return $this
*/
public function setCaption(array|RichText|string $caption): static
{
$this->caption = $caption;
return $this;
}
/**
* Get allow overlay of other elements?
*/
public function getOverlay(): bool
{
return $this->overlay;
}
/**
* Set allow overlay of other elements?
*/
public function setOverlay(bool $overlay): self
{
$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;
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$this->layout = ($this->layout === null) ? null : clone $this->layout;
$this->font = ($this->font === null) ? null : clone $this->font;
if (is_array($this->caption)) {
$captions = $this->caption;
$this->caption = [];
foreach ($captions as $caption) {
$this->caption[] = is_object($caption) ? (clone $caption) : $caption;
}
} else {
$this->caption = is_object($this->caption) ? (clone $this->caption) : $this->caption;
}
}
}

View file

@ -0,0 +1,217 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart;
class TrendLine extends Properties
{
const TRENDLINE_EXPONENTIAL = 'exp';
const TRENDLINE_LINEAR = 'linear';
const TRENDLINE_LOGARITHMIC = 'log';
const TRENDLINE_POLYNOMIAL = 'poly'; // + 'order'
const TRENDLINE_POWER = 'power';
const TRENDLINE_MOVING_AVG = 'movingAvg'; // + 'period'
const TRENDLINE_TYPES = [
self::TRENDLINE_EXPONENTIAL,
self::TRENDLINE_LINEAR,
self::TRENDLINE_LOGARITHMIC,
self::TRENDLINE_POLYNOMIAL,
self::TRENDLINE_POWER,
self::TRENDLINE_MOVING_AVG,
];
private string $trendLineType = 'linear'; // TRENDLINE_LINEAR
private int $order = 2;
private int $period = 3;
private bool $dispRSqr = false;
private bool $dispEq = false;
private string $name = '';
private float $backward = 0.0;
private float $forward = 0.0;
private float $intercept = 0.0;
/**
* Create a new TrendLine object.
*/
public function __construct(
string $trendLineType = '',
?int $order = null,
?int $period = null,
bool $dispRSqr = false,
bool $dispEq = false,
?float $backward = null,
?float $forward = null,
?float $intercept = null,
?string $name = null
) {
parent::__construct();
$this->setTrendLineProperties(
$trendLineType,
$order,
$period,
$dispRSqr,
$dispEq,
$backward,
$forward,
$intercept,
$name
);
}
public function getTrendLineType(): string
{
return $this->trendLineType;
}
public function setTrendLineType(string $trendLineType): self
{
$this->trendLineType = $trendLineType;
return $this;
}
public function getOrder(): int
{
return $this->order;
}
public function setOrder(int $order): self
{
$this->order = $order;
return $this;
}
public function getPeriod(): int
{
return $this->period;
}
public function setPeriod(int $period): self
{
$this->period = $period;
return $this;
}
public function getDispRSqr(): bool
{
return $this->dispRSqr;
}
public function setDispRSqr(bool $dispRSqr): self
{
$this->dispRSqr = $dispRSqr;
return $this;
}
public function getDispEq(): bool
{
return $this->dispEq;
}
public function setDispEq(bool $dispEq): self
{
$this->dispEq = $dispEq;
return $this;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getBackward(): float
{
return $this->backward;
}
public function setBackward(float $backward): self
{
$this->backward = $backward;
return $this;
}
public function getForward(): float
{
return $this->forward;
}
public function setForward(float $forward): self
{
$this->forward = $forward;
return $this;
}
public function getIntercept(): float
{
return $this->intercept;
}
public function setIntercept(float $intercept): self
{
$this->intercept = $intercept;
return $this;
}
public function setTrendLineProperties(
?string $trendLineType = null,
?int $order = 0,
?int $period = 0,
?bool $dispRSqr = false,
?bool $dispEq = false,
?float $backward = null,
?float $forward = null,
?float $intercept = null,
?string $name = null
): self {
if (!empty($trendLineType)) {
$this->setTrendLineType($trendLineType);
}
if ($order !== null) {
$this->setOrder($order);
}
if ($period !== null) {
$this->setPeriod($period);
}
if ($dispRSqr !== null) {
$this->setDispRSqr($dispRSqr);
}
if ($dispEq !== null) {
$this->setDispEq($dispEq);
}
if ($backward !== null) {
$this->setBackward($backward);
}
if ($forward !== null) {
$this->setForward($forward);
}
if ($intercept !== null) {
$this->setIntercept($intercept);
}
if ($name !== null) {
$this->setName($name);
}
return $this;
}
}