Merge branch 'master' into Matrix-Arithmetic-Value-Testing

This commit is contained in:
Mark Baker 2022-08-07 14:07:31 +02:00 committed by GitHub
commit b627770ece
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 924 additions and 860 deletions

View File

@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org).
- Implementation of the new `TEXTBEFORE()`, `TEXTAFTER()` and `TEXTSPLIT()` Excel Functions - Implementation of the new `TEXTBEFORE()`, `TEXTAFTER()` and `TEXTSPLIT()` Excel Functions
- Implementation of the `ARRAYTOTEXT()` Excel Function - Implementation of the `ARRAYTOTEXT()` Excel Function
- Support for [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) implementation of
JpGraph library to render charts added.
### Changed ### Changed

View File

@ -71,16 +71,18 @@ or the appropriate PDF Writer wrapper for the library that you have chosen to in
#### Chart Export #### Chart Export
For Chart export, we support, which you will also need to install yourself For Chart export, we support following packages, which you will also need to install yourself using `composer require`
- jpgraph/jpgraph - [jpgraph/jpgraph](https://packagist.org/packages/jpgraph/jpgraph) (this package was abandoned at version 4.0.
You can manually download the latest version that supports PHP 8 and above from [jpgraph.net](https://jpgraph.net/))
- [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) (fork with php 8.1 support)
and then configure PhpSpreadsheet using: and then configure PhpSpreadsheet using:
```php ```php
Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); // to use jpgraph/jpgraph
//or
Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); // to use mitoteam/jpgraph
``` ```
You can `composer/require` the github version of jpgraph, but this was abandoned at version 4.0; or manually download the latest version that supports PHP 8 and above from [jpgraph.net](https://jpgraph.net/)
## Documentation ## Documentation
Read more about it, including install instructions, in the [official documentation](https://phpspreadsheet.readthedocs.io). Or check out the [API documentation](https://phpoffice.github.io/PhpSpreadsheet). Read more about it, including install instructions, in the [official documentation](https://phpspreadsheet.readthedocs.io). Or check out the [API documentation](https://phpoffice.github.io/PhpSpreadsheet).

View File

@ -11,6 +11,8 @@ parameters:
- tests/ - tests/
excludePaths: excludePaths:
- src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - src/PhpSpreadsheet/Chart/Renderer/JpGraph.php
- src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php
- src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php
parallel: parallel:
processTimeout: 300.0 processTimeout: 300.0
checkMissingIterableValueType: false checkMissingIterableValueType: false

View File

@ -2,68 +2,20 @@
namespace PhpOffice\PhpSpreadsheet\Chart\Renderer; 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;
/** /**
* Jpgraph is not maintained in Composer, and the version there * Jpgraph is not oficially maintained in Composer, so the version there
* is extremely out of date. For that reason, all unit test * could be out of date. For that reason, all unit test requiring Jpgraph
* requiring Jpgraph are skipped. So, do not measure * are skipped. So, do not measure code coverage for this class till that
* code coverage for this class till that is fixed. * is fixed.
*
* This implementation uses abandoned package
* https://packagist.org/packages/jpgraph/jpgraph
* *
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
class JpGraph implements IRenderer class JpGraph extends JpGraphRendererBase
{ {
private static $width = 640; protected static function init(): void
private static $height = 480;
private static $colourSet = [
'mediumpurple1', 'palegreen3', 'gold1', 'cadetblue1',
'darkmagenta', 'coral', 'dodgerblue3', 'eggplant',
'mediumblue', 'magenta', 'sandybrown', 'cyan',
'firebrick1', 'forestgreen', 'deeppink4', 'darkolivegreen',
'goldenrod2',
];
private static $markSet;
private $chart;
private $graph;
private static $plotColour = 0;
private static $plotMark = 0;
/**
* Create a new jpgraph.
*/
public function __construct(Chart $chart)
{
self::init();
$this->graph = null;
$this->chart = $chart;
}
private static function init(): void
{ {
static $loaded = false; static $loaded = false;
if ($loaded) { if ($loaded) {
@ -81,802 +33,6 @@ class JpGraph implements IRenderer
\JpGraph\JpGraph::module('scatter'); \JpGraph\JpGraph::module('scatter');
\JpGraph\JpGraph::module('stock'); \JpGraph\JpGraph::module('stock');
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,
];
$loaded = true; $loaded = true;
} }
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($groupID, $datasetLabels, $labelCount, $rotation = '')
{
$datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode();
if ($datasetLabelFormatCode !== null) {
// 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($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($dataValues, $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($type = 'textlin'): void
{
$this->graph = new Graph(self::$width, self::$height);
$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(self::$width, self::$height);
$this->renderTitle();
}
private function renderRadarPlotArea(): void
{
$this->graph = new RadarGraph(self::$width, self::$height);
$this->graph->SetScale('lin');
$this->renderTitle();
}
private function renderPlotLine($groupID, $filled = false, $combination = false, $dimensions = '2d'): void
{
$grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping();
$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, $labelCount);
$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) {
$dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
$marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->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($i)->getDataValue();
$seriesPlot->SetLegend($dataLabel);
$seriesPlots[] = $seriesPlot;
}
if ($grouping == 'standard') {
$groupPlot = $seriesPlots;
} else {
$groupPlot = new AccLinePlot($seriesPlots);
}
$this->graph->Add($groupPlot);
}
private function renderPlotBar($groupID, $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();
$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, $labelCount, $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) {
$dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->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($groupID, $bubble): void
{
$grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping();
$scatterStyle = $bubbleSize = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle();
$seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
$seriesPlots = [];
// 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();
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) * self::$width / 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($groupID): void
{
$radarStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle();
$seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
$seriesPlots = [];
// 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] = implode(' ', array_reverse($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($groupID): void
{
$contourStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle();
$seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
$seriesPlots = [];
$dataValues = [];
// 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();
$dataValues[$i] = $dataValuesX;
}
$seriesPlot = new ContourPlot($dataValues);
$this->graph->Add($seriesPlot);
}
private function renderPlotStock($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)->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];
}
}
// 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, $labelCount);
$this->graph->xaxis->SetTickLabels($datasetLabels);
}
$seriesPlot = new StockPlot($dataValuesPlot);
$seriesPlot->SetWidth(20);
$this->graph->Add($seriesPlot);
}
private function renderAreaChart($groupCount, $dimensions = '2d'): void
{
$this->renderCartesianPlotArea();
for ($i = 0; $i < $groupCount; ++$i) {
$this->renderPlotLine($i, true, false, $dimensions);
}
}
private function renderLineChart($groupCount, $dimensions = '2d'): void
{
$this->renderCartesianPlotArea();
for ($i = 0; $i < $groupCount; ++$i) {
$this->renderPlotLine($i, false, false, $dimensions);
}
}
private function renderBarChart($groupCount, $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, $dimensions = '2d', $doughnut = false, $multiplePlots = false): void
{
$this->renderPiePlotArea();
$iLimit = ($multiplePlots) ? $groupCount : 1;
for ($groupID = 0; $groupID < $iLimit; ++$groupID) {
$grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping();
$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, $labelCount);
}
}
$seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
$seriesPlots = [];
// 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) {
$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, $dimensions): void
{
$this->renderCartesianPlotArea('intint');
for ($i = 0; $i < $groupCount; ++$i) {
$this->renderPlotContour($i);
}
}
private function renderCombinationChart($groupCount, $dimensions, $outputDestination)
{
$this->renderCartesianPlotArea();
for ($i = 0; $i < $groupCount; ++$i) {
$dimensions = null;
$chartType = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType();
switch ($chartType) {
case 'area3DChart':
$dimensions = '3d';
// no break
case 'areaChart':
$this->renderPlotLine($i, true, true, $dimensions);
break;
case 'bar3DChart':
$dimensions = '3d';
// no break
case 'barChart':
$this->renderPlotBar($i, $dimensions);
break;
case 'line3DChart':
$dimensions = '3d';
// no break
case 'lineChart':
$this->renderPlotLine($i, false, true, $dimensions);
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($outputDestination)
{
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, $dimensions, $outputDestination);
}
}
switch ($chartType) {
case 'area3DChart':
$dimensions = '3d';
// no break
case 'areaChart':
$this->renderAreaChart($groupCount, $dimensions);
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, $dimensions);
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':
$dimensions = '3d';
// no break
case 'surfaceChart':
$this->renderContourChart($groupCount, $dimensions);
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,861 @@
<?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 static $width = 640;
private static $height = 480;
private static $colourSet = [
'mediumpurple1', 'palegreen3', 'gold1', 'cadetblue1',
'darkmagenta', 'coral', 'dodgerblue3', 'eggplant',
'mediumblue', 'magenta', 'sandybrown', 'cyan',
'firebrick1', 'forestgreen', 'deeppink4', 'darkolivegreen',
'goldenrod2',
];
private static $markSet;
private $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,
];
}
/**
* 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($groupID, $datasetLabels, $labelCount, $rotation = '')
{
$datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode();
if ($datasetLabelFormatCode !== null) {
// 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($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($dataValues, $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($type = 'textlin'): void
{
$this->graph = new Graph(self::$width, self::$height);
$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(self::$width, self::$height);
$this->renderTitle();
}
private function renderRadarPlotArea(): void
{
$this->graph = new RadarGraph(self::$width, self::$height);
$this->graph->SetScale('lin');
$this->renderTitle();
}
private function renderPlotLine($groupID, $filled = false, $combination = false, $dimensions = '2d'): void
{
$grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping();
$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, $labelCount);
$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) {
$dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
$marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->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($i)->getDataValue();
$seriesPlot->SetLegend($dataLabel);
$seriesPlots[] = $seriesPlot;
}
if ($grouping == 'standard') {
$groupPlot = $seriesPlots;
} else {
$groupPlot = new AccLinePlot($seriesPlots);
}
$this->graph->Add($groupPlot);
}
private function renderPlotBar($groupID, $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();
$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, $labelCount, $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) {
$dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->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($groupID, $bubble): void
{
$grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping();
$scatterStyle = $bubbleSize = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle();
$seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
$seriesPlots = [];
// 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();
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) * self::$width / 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($groupID): void
{
$radarStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle();
$seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
$seriesPlots = [];
// 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] = implode(' ', array_reverse($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($groupID): void
{
$contourStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle();
$seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
$seriesPlots = [];
$dataValues = [];
// 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();
$dataValues[$i] = $dataValuesX;
}
$seriesPlot = new ContourPlot($dataValues);
$this->graph->Add($seriesPlot);
}
private function renderPlotStock($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)->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];
}
}
// 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, $labelCount);
$this->graph->xaxis->SetTickLabels($datasetLabels);
}
$seriesPlot = new StockPlot($dataValuesPlot);
$seriesPlot->SetWidth(20);
$this->graph->Add($seriesPlot);
}
private function renderAreaChart($groupCount, $dimensions = '2d'): void
{
$this->renderCartesianPlotArea();
for ($i = 0; $i < $groupCount; ++$i) {
$this->renderPlotLine($i, true, false, $dimensions);
}
}
private function renderLineChart($groupCount, $dimensions = '2d'): void
{
$this->renderCartesianPlotArea();
for ($i = 0; $i < $groupCount; ++$i) {
$this->renderPlotLine($i, false, false, $dimensions);
}
}
private function renderBarChart($groupCount, $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, $dimensions = '2d', $doughnut = false, $multiplePlots = false): void
{
$this->renderPiePlotArea();
$iLimit = ($multiplePlots) ? $groupCount : 1;
for ($groupID = 0; $groupID < $iLimit; ++$groupID) {
$grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping();
$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, $labelCount);
}
}
$seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount();
$seriesPlots = [];
// 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) {
$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, $dimensions): void
{
$this->renderCartesianPlotArea('intint');
for ($i = 0; $i < $groupCount; ++$i) {
$this->renderPlotContour($i);
}
}
private function renderCombinationChart($groupCount, $dimensions, $outputDestination)
{
$this->renderCartesianPlotArea();
for ($i = 0; $i < $groupCount; ++$i) {
$dimensions = null;
$chartType = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType();
switch ($chartType) {
case 'area3DChart':
$dimensions = '3d';
// no break
case 'areaChart':
$this->renderPlotLine($i, true, true, $dimensions);
break;
case 'bar3DChart':
$dimensions = '3d';
// no break
case 'barChart':
$this->renderPlotBar($i, $dimensions);
break;
case 'line3DChart':
$dimensions = '3d';
// no break
case 'lineChart':
$this->renderPlotLine($i, false, true, $dimensions);
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($outputDestination)
{
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, $dimensions, $outputDestination);
}
}
switch ($chartType) {
case 'area3DChart':
$dimensions = '3d';
// no break
case 'areaChart':
$this->renderAreaChart($groupCount, $dimensions);
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, $dimensions);
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':
$dimensions = '3d';
// no break
case 'surfaceChart':
$this->renderContourChart($groupCount, $dimensions);
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;
/**
* Jpgraph is not oficially maintained in Composer.
*
* This renderer implementation uses package
* https://packagist.org/packages/mitoteam/jpgraph
*
* This package is up to date for August 2022 and has PHP 8.1 support.
*
* @codeCoverageIgnore
*/
class MtJpGraphRenderer extends JpGraphRendererBase
{
protected static function init(): void
{
static $loaded = false;
if ($loaded) {
return;
}
\mitoteam\jpgraph\MtJpGraph::load([
'bar',
'contour',
'line',
'pie',
'pie3d',
'radar',
'regstat',
'scatter',
'stock',
]);
$loaded = true;
}
}

View File

@ -5,7 +5,10 @@ GraPHPite
http://graphpite.sourceforge.net/ http://graphpite.sourceforge.net/
JpGraph JpGraph
http://www.aditus.nu/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 LibChart
https://naku.dohcrew.com/libchart/pages/introduction/ https://naku.dohcrew.com/libchart/pages/introduction/